How do you keep track of complex story within your script?

Started by Tarnos12, Fri 22/07/2022 00:15:02

Previous topic - Next topic

Tarnos12

Hey,

I've been trying different things, but all came down to keeping track of an integer which goes up as you do things related to the story.
This number then can be used in your dialog(and other places) to change characters dialogs based on how far the story has progressed etc.

Recently I've made a small module that keeps track of multiple numbers and I am using an enum to represent those numbers.
This allows me to do:
Code: ags

enum StoryProgression
{
    eStory_Hallway = 1,//start at 1, because the "default" story uses index 0
    eStory_Drawer,
    eStory_KitchenSink,
    eStory_John
}
StoryProgress.SetId(1);//default story with index "0"
StoryProgress.SetId(1, eStory_Hallway);// Set hallway scene/story to ID 1.


Code: ags

//John Dialog
if(StoryProgress.GetId() == 1)
{
 cJohn.Say("The game just started");
}
else if(StoryProgress.GetId() == 2)
{
   if(StoryProgress.GetId(eStory_Hallway) == 2)
  {
    //Hallway story is 2 and general story is 2.
    cJohn.Say("Talked to the John, progressed the story, hallway story allowed this conversation to happen");
    StoryProgress.SetId(3);//general story is 3
  }
  else
  {
     cJhon.Say("Go to hallway and do your thing");
  }
}

//Dialog in a hallway, like interacting with a Vase or something
if(StoryProgress.GetId(eStory_Hallway) == 1)
{
  cPlayer.AddInventory(iVaseKey);
  cPlayer.Say("Found hidden Key behind the Vase");
  StoryProgress.SetId(2, eStory_Hallway);//progress the story, so you can't obtain the key multiple times 
}
else if(StoryProgress.GetId(eStory_Hallway) == 2)
{
  cPlayer.Say("Nothing to find here, already got the key");
}


Unfortunately this gets harder with larger story and more pieces.
So my next idea is to use enums for the story ID as well.
So instead of following the number, I can use the name.

If you click on the door at the start of the game it will do certain thing/character will say something specific, but when you do it again later in the story, the door might just open and/or character say something new and other things may happen.
I don't want to write multiple if statements to check the conditions, the idea of using the story ID is to not think about conditions such as having an inventory item or being in a correct room or having talked to Peter, after eating the breakfast, taking a nap and the game being on Day 5.
This could be achieved with some numbers(or enums if the story gets too complicated and you can't keep up with the numbers)

This is used together with a script that is executed each time a story Id is changed, so you can change the state of characters/world when you reach certain story point.

What kind of systems do you use in your games?
How do you change the state of various objects/characters in the game?

EDIT:I should add that one of the ways of simplifying it is to use the general story ID as a way to separate the game into Chapters which helps, but the issue is that some characters/objects won't change their dialogs with a new chapter, so you either have to duplicate the dialog or not use chapters for those special cases.

This comes down to this:
1) Too many numbers for every little thing that keeps track of the state of the game("main quest") and optional stuff("side quests")
2) Use enums/strings instead of numbers so it's less arbitrary and has some meaning?
3) Other solutions? A way to make a character say different things based on multiple conditions without actually writing down those conditions in a big if statement?

newwaveburritos

This was essentially the central question of my first game and learning how to do this effectively is why it is such a mess in the code right now and why it probably won't see the light of day unless I just take the assets and start over.

I guess I'll just start at the beginning here and explain everything i did because maybe somebody will read this and learn something from what I have done since the whole thing was a long learning process for me.  I don't know if I really have much here that you can glean much from because it's mostly a long list of what not to do lol.

I decided early to divide the game into days like chapters to try and also make the organization of tasks a bit more streamlined.  I had an integer like you're talking about.  So, every interaction with anything would start with a check against that.  if (WhatDayIsIt==2) I would know that it is Night #1 and proceed accordingly.  WhatDayIsIt==3 would be Day#2 and so forth.  Then if I wanted to track the progress I would just create a new global variable and name it something halfway intelligent and check that.  Obviously, that becomes very confusing very quickly because it doesn't matter how smart your variable name is, it's going to get very confusing if you have a bunch of them.  Eventually, I started trying to use get the variables under control by using fewer of them but then I got into trouble because I was doing something like GameProgress++; which means you can get extra credit for extra clicking.  Whoops.  That led to a lot of tracking down weird reactions from characters.  Eventually I started doing this:

Code: ags

if (Game.DoOnceOnly("IntroductionToPeter")){
  GameProgress++;
}


I don't know why it didn't occur to me to just set it to a number.  Probably because I was trying to do things like you need to talk to Peter, eat breakfast, and take a nap but actually you can satisfy the requirement by either eating breakfast OR taking a nap but also doing all three would be enough so in this case GameProgress can be either 2 or 3.  But I don't know because I was doing a lot of bullshit code at the time.  But tracking the game state became such a mess that I never wanted to look at it again despite the fact that i was also very attached to it and the characters and had spent a year working on it.  Eventually I learned about enums and this seemed like the way to do it but by then it wasn't really much help.

The second game I made was for MAGS so it was short and pretty dang linear so I didn't really need to track a lot of states.  Now the game I'm working on is getting to a certain level of complexity and I'm starting to ask this question again so I'm glad you asked it since this is exactly what I intend to work on next once I stop stalling and working on extra animations for no real reason and just get on with the meat of the gameplay.  I really like the idea of using an enum but like Crimson Wizard said on the Discord that it also depends greatly on the number of parallel states.  I also thing tracking a non-linear story would be a lot harder to do than just getting from point A to point B.  My natural inclination is to go with a sort of Monkey Island setup where you have more than one task that you can be working on at any one time.  If you can't find Dracula's head shots maybe you can work on the circuit board puzzle for awhile and then maybe you'll figure something out while you do that.  I saw Roberta Williams plan out a game on a gigantic piece of paper in some documentary so I also think that might be a useful thing that's essentially half a map and half a puzzle dependency chart.  At the end of the day I think you're just trying to come up with a way that makes sense to you to keep track of that number going up and so it might make sense for one person to use an enum and another to do it some other way.  It feels like it might be kinda personal.

How would you use your system to track parallel states?  I guess you wouldn't really need to exactly since you could just use one for each and then check both of them when it's time to escape the dungeon or whatever.

There was a little chatter about this awhile ago that I read you may find illuminating although it doesn't go into a ton of depth:

https://www.adventuregamestudio.co.uk/forums/index.php?topic=56638.msg636598246#msg636598246

Now that I read that all back to myself it's kind of a jumbled mess.  Maybe I should have used an enum to track the progress of whatever point I was trying to make.

Tarnos12

Quote from: newwaveburritos on Fri 22/07/2022 05:28:57
How would you use your system to track parallel states?  I guess you wouldn't really need to exactly since you could just use one for each and then check both of them when it's time to escape the dungeon or whatever.

There are at least 2 ways I can see with the current system.

1)
Code: ags


// Chapter 1/Day 1
if(StoryProgress.GetId() == 1)
{
    if(StoryProgress.GetId(eStory_Hallway) == 2 && StoryProgress.GetId(eStory_John) == 2)
    {
        StoryProgress.SetId(2);//Change chapter/day to 2.
    }
}
//Chapter 2/Day 2
else if(StoryProgresss.GetId() == 2)
{
    //Chapter 1 is no longer of concern.
    //But this comes back to the issue where you might have interactions that do not change between chapters/days.(which is not too bad, you should call separate dialog which takes care of that or even separate dialogs into chapters/states)
}


2)
Code: ags


//Hallway + John story are at 2.
if(StoryProgress.GetId(eStory_Hallway) == 2 && StoryProgress.GetId(eStory_John) == 2)
{
     //John is ready + hallway is ready
}
// Since previous condition failed, and if this succeeded that means John wasn't at story 2, but hallway was.
else if(StoryProgress.GetId(eStory_Hallway) == 2)
{
    // John is not ready yet
}


3) This is similar to what I do currently

Code: ags

if(InteractedWith(eCharacter_John)
{
    if(StoryProgress.GetId(eStory_John) == 0)
    {
         cJohn.SayCustom("The game just started, it's probably first time we talk");
         StoryProgress.SetId(1, eStory_John);//Progress John story
     }
     else if(StoryProgress.GetId(eStory_John) == 1)
     {
          cJohn.SayCustom("Go talk to bob");
          //We do not increase john story id, it will be done at different point, he will keep repeating this dialog until we make progress elsewhere.
      }
    //John story requires to be 2, you need to do something else to progress it, like  talk to Bob or combine items or enter different area etc. 
    if(StoryProgress.GetId(eStory_John) == 2)
    {
           if(StoryProgress.GetId(eStory_Hallway) == 2)
           {
              //John is ready + hallway is ready
           }
          else
          {
              // John is not ready yet
          }
     }
}


You can also nest if statements which I like more as long as I don't go too deep(and you probably don't need different state for all different sub states.
For example, hallway, john, kitchen, vase.
There is no need for Bob to say 20 different things based on the mix of those 4(or more) states(Unless you really want that I guess)
In a 3rd example I am separating interactions into their own states(if they have a state of their own, usually all characters do since they are going to say different things and objects might just use "Game.DoOnceOnly")


Also there is a module that allows you to write multiple dialogs in 1 line that will be used next time you talk to the character, I think it's called MultiText:
Code: ags

cPlayer.SayLoop("Hello|Hi|Can I help you?");//Each time you interact with a character different message will play.

Danvzare

Hey, I use a number to keep track of the story as well.  :-D
Sometimes though (when I'm feeling too lazy to declare a single variable) I use a location of a character. Since I know I can just throw a character into the main menu room and then check that to see where you are in the story.

The only major problem I've encountered so far with using a number, is if you decide to expand on the story from the middle. You either need a make a new variable or push every other check up by one. (And looking for every time that variable is checked can be a pain.)
Another problem is that it can sometimes be difficult to know where you are in the story with a simple number, but I usually include some notes in the global script, so I know where in the story each number represents.

Crimson Wizard

#4
The solution to a non-linear story tracking (may be used for linear as well) is a Node Graph. You already sort of have one, only scripted in a workaround way. If scripted literally, it may also solve an issue of having to script story switches "in place".

Following is a very rough outline, but I hope it should illustrate the idea. I apologize for a messy reply beforehand, but I fear to not have enough spare time to write all this thoroughly.




A Node Graph consists of Nodes. A Node is (in pseudocode):

Code: ags

// pseudo code!!!
struct Node {
    ID - some way to identify it when calling a function, a number, a string, and so on.
    Name - a human-readable node name, useful for debugging.
    Description - game specific parameters, as become necessary.
    Links - list of connected node IDs, or whatever allows to reference them.
        There may be separate arrays of Forward links and Backward links too, if you need to let a node know which prerequisite nodes connect to it.
};


You may store these nodes any way you prefer, or any way the current scripting/programming language allows. It may be one huge array, or separate Node objects linked to others using pointers (currently impossible in AGS, sadly).

This kind of structure makes it trivial to setup a complex story. You no longer have to make huge "if/else if" lists when deciding what stage to switch to. Instead, you configure the node list at the start of the game (or when loading some data), where you create and link nodes.

Code: ags

// pseudo code!!!
    NodeGraph.CreateNode("John Story 1");
    NodeGraph.CreateNode("John Story 2");
    NodeGraph.LinkNodes("John Story 1", "John Story 2");


And then have a single function, let's say "CompleteNode", that marks particular node as completed and advance to others, found by this node links.

This is where we meet a question of how to track active nodes. The most dumb straightforward way is to make each Node have "Completed" and "Active" parameters. In which case the progression may look like:

Code: ags

// pseudo code!!!
function CompleteNode( Node node ) {
     node.Active = false;
     node.Completed = true;
     // Check and update each *following* node
     for each fwnode in node.ForwardLinks {
        bool must_activate = true;
        // For each of the forward node, check if all the preceding nodes are completed
        for each bwnode in fwnode.BackwardLinks
            if (!bwnode.Completed) {
                must_activate = false;
            }
        }
        // If all is completed, then activate this forward node
        if (must_activate) {
            fwnode.Active = true;
        }
     }
}


This approach is very simple, but it has one issue: we don't have a easily accessed list of active nodes.
(Actually, there's another: a node graph describes a "current position" in itself, which is conceptually wrong in my opinion, but in practice this does not matter so long as there's only one protagonist that may traverse a graph.)

How this may be solved?

Firstly, you may have a literal array of active nodes, which you update in "CompleteNode".

Secondly, following your current idea of having separate "Stories", a Node may have a Story Branch reference (a "Story branch" it belongs to). If it does, then you may have some kind of a Story Branch object, pointing to the active Node:

Code: ags

// pseudo code!!!
function CompleteNode( Node node ) {
    <............>
        // If all is completed, then activate this forward node
        if (must_activate) {
            fwnode.Active = true;
            StoryBranch story = fwnode.Story;
            story.CurrentNode = fwnode;
        }
}


Then, when testing for the current story stage, you could do something like:

Code: ags

// pseudo code!!!
function GetStoryNodeID( StoryBranch story ) {
     return story.CurrentNode.ID;
}

function DoSomethingForJohn() {
    int id = GetStoryNodeID(eStory_John);
    if (id == eStory_John_Begins) {
        // do this
    } else if (id == eStory_John_Continues) {
        // do that
    }
}


Another approach to tracking active nodes is Node Cursors. Node Cursor (or Story Cursor) is a struct that references current node, and then advanced by "node completed" command, along the links. The problem here is to invent a solution of having multiple cursors restricted to subbranches. So I won't go there now, as above Story references may already suffice (actually, it may be very similar thing).




The biggest advantage of a node graph, in my opinion, is that you don't have to keep the whole story in mind when scripting a reaction to some interaction. Say, if there's an object, and interacting with it advanced a story, all you have to do is to create a new Node for this object and connect it to the story.

Code: ags

// pseudo code!!!

    NodeGraph.CreateNode("Yellow Door Node");
    NodeGraph.LinkNodes("Yellow Door Node", "John Story 10");



Then, when an object is interacted, you simply do "CompleteNode( my_node )", and the whole system will be updated accordingly.

Tarnos12

Quote from: Danvzare on Fri 22/07/2022 15:59:28
Hey, I use a number to keep track of the story as well.  :-D
Sometimes though (when I'm feeling too lazy to declare a single variable) I use a location of a character. Since I know I can just throw a character into the main menu room and then check that to see where you are in the story.

The only major problem I've encountered so far with using a number, is if you decide to expand on the story from the middle. You either need a make a new variable or push every other check up by one. (And looking for every time that variable is checked can be a pain.)
Another problem is that it can sometimes be difficult to know where you are in the story with a simple number, but I usually include some notes in the global script, so I know where in the story each number represents.

The solution that worked for us was to write a story in larger chunks.
Instead of incrementing the story by 1, do it by 10.
Or separate it in different chunks like:

0-250 = Day 1 Morning
251-500 = Day 1 Afternoon
501-750 = Day 1 Evening
750- 999 = Day 1 Night

Then You just write a story in smaller chunks like ID 10,20,30.
This way you can fit other things later on.
And if you expect the story to get even bigger then go with 0-2500 and increment story parts by 100 to give yourself more space.

Tarnos12

Quote from: Crimson Wizard on Fri 22/07/2022 16:27:50
The solution to a non-linear story tracking (may be used for linear as well) is a Node Graph. You already sort of have one, only scripted in a workaround way. If scripted literally, it may also solve an issue of having to script story switches "in place".

Following is a very rough outline, but I hope it should illustrate the idea. I apologize for a messy reply beforehand, but I fear to not have enough spare time to write all this thoroughly.

Thanks, that looks really good.

I need to take my time reading through this few times, but it seems like a great solution so far.
It basically removes the need to think about numbers and instead you think in terms of the node/story branch.

This will probably be the next step in improving the story progression in my games.
I will play around with it in the next few days as I have a bit of time before I will be busy and I will update with my progress :D

Also I did read about Nodes in some C# course I took in the past, it is one of the data types(Google says it's called a "Linked List")
I had it in the back of my head for a long time, but this might be a good time to try it out.

Pax Animo

Interesting read,

I'm always looking for ways to keep my plots well structured, main or sub.

At the moment I'm using something along the lines of this:

Code: ags
if (story[0]) {
    lTaskTitle.Text = "Prologue: 0-0";
    lTask.Text = "Make conversation.";
    dPrologue00.Start();
  }


An array of bools which can only be one or the other so it's easy(ish) to debug/modify, and then i'm using dialogues to break the code into chunks.
I really like the dialogue system in AGS as it's a great way to separate a lot of code into handy little "sub scripts" so to speak.

Possibly creating new bool arrays for new chapters to keep things a bit easier to read, i'm only in the early stages of using this approach so i'm sure i'll run into issues along the way at some point.
Misunderstood

newwaveburritos

I was talking about this topic with a buddy of mine and he had a pretty good idea for tracking game progress.  It was useful for me so I thought I would post it here.  That is, I'm always trying to update a game state and it turns out that that's really not that useful.  What is more useful for me, since my games tend to be very character driven is to keep track of the character state instead.  This is much easier for me to visualize than trying to keep some kind of nebulous game state with many moving parts.  If I know the position a character is in then I can have them interact with the player accordingly.  Game sates can be reserved for situations that don't have any bearing on the characters for more generic stuff.  Anyway, just thought I would share this.

newwaveburritos

A secondary question here which is more of a planning and strategy question than anything:

Do you guys have a story written and ready to go before you begin or do you make it up as you go?  Or a little from Column A and a little from Column B?  I'm starting to think that a large part of my problem with this is because I don't really have a whole story planned out but rather I just kind of make it up as I go from a vague outline.

Tarnos12

Quote from: newwaveburritos on Fri 29/07/2022 20:54:43
A secondary question here which is more of a planning and strategy question than anything:

Do you guys have a story written and ready to go before you begin or do you make it up as you go?  Or a little from Column A and a little from Column B?  I'm starting to think that a large part of my problem with this is because I don't really have a whole story planned out but rather I just kind of make it up as I go from a vague outline.

In our case, we always have a full story ready with most/all puzzles.
We might add/remove some things during the development.

timid_dolphin

I'm hoping 'game state' won't matter too much, provided that the story progresses only by player actions.

My philosophy is that there should be a direct cause and effect relationship between anything which happens, always beginning with choices made by the player.

for example:

Good =
cat owner mentions their cat is frightened of loud noises and happens to have valuable item player wants for some other purpose...
player pops balloon, cat gets scared and runs away, lost cat poster appears, lure cat out with toy, pick up cat, return cat to owner, collect reward.

Bad =
player enters room 4 on Tuesday while holding a banana, the sun rises. the player can now use the magnifying glass to start a fire. Surprise! Fire summons alien spaceship for some reason.

----
I think there's a moment at the start of LSL5 where you need to collect various items so that the limo will arrive to take you to the next area. This is obviously done as a band-aid solution to a design full of dead-man walking situations.

My theory is that if you mostly stick to this design style keeping track of game state should isn't really necessary, but you need to keep track of specific things which matter, eg what time of day it is, what information does a specific character have, what inv items does a character have, has the player witnessed something (eg a cutscene) yet, that sort of thing.

Tarnos12

Quote from: timid_dolphin on Sat 30/07/2022 05:04:35
My theory is that if you mostly stick to this design style keeping track of game state should isn't really necessary, but you need to keep track of specific things which matter, eg what time of day it is, what information does a specific character have, what inv items does a character have, has the player witnessed something (eg a cutscene) yet, that sort of thing.
We are trying to avoid having to keep track of that.
I mean you still have to keep track of it, but you don't actively have to remember every little detail when you write dialogs etc.

SMF spam blocked by CleanTalk