Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

My sequence editor for our point-n-click game (What does your look like?)

Discussion in 'Scripting' started by nicmarxp, Mar 27, 2019.

  1. nicmarxp

    nicmarxp

    Joined:
    Dec 3, 2017
    Posts:
    406
    We're making a point-n-click game, and when you interact with items, stuff should happen, for example:

    - Speak "What is this..?"
    - Wait 2 seconds
    - Animated pulling lever
    - Start particle system
    - Speak "Wow, that was cool!"

    I'm in the process of adding if-statements and variables, to for example let the character say a thing only once, and the next time he interacts, say something else.

    My Interaction-script contains a List of InteractSequence, that contains a delay and an
    InteractionEvent. Then each InteractionEvent runs in order with the set delay.

    InteractionEvent has multiple InteractionEventTypes, like Speak, Audio, Particlesystem, Animate Transform etc. So I was thinking of adding branching to this, like SetVariable and If. I'm not sure if this will work, but I will try.

    I liked what Pixelcrushers did in their Dialogue system, where they use a Lua-script, and the game designer can click to create actions/events, but I'm not sure if Lua is the way to go. or if I should pursue the current method.

    It would be interesting to get ideas on how anyone else has solved this. Especially in setting variables. It would be nice if I didn't have to predefine them like how Dialogue system did it.

    Here's a screenshot of what it currently looks like. I'm also using Odin Inspector to make it look nicer.



    I'm looking forward to hear what you think!

    -Niclas @ Blue Goo Games :)
     
  2. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,670
    I think Lua works well for the Dialogue System for a few reasons that are particular to dialogue systems:
    • Authors can choose to write Lua directly, allowing them to stay in the flow of writing text instead of fiddling with UIs. (Same with the Dialogue System's sequencer commands.) Or they can use the dropdowns if preferred.
    • Text is much cleaner to export and import to external formats such as Excel for localization.
    • It's what Chat Mapper uses. Authors familiar with Chat Mapper or articy:draft can continue to use those tools. (For articy, the Dialogue System translates articy:expresso to Lua.) If they decide to jump ship entirely to the Dialogue System, this also makes the learning curve very easy.
    A secondary advantage is that a lot of devs have been interested in writing moddable RPGs. Having Lua under the hood makes it much, much easier.

    That said, for a general-purpose non-interactive sequence editor, I like using timeline-based editors like SLATE, Cinema Director, and Unity Timeline. It's nice to be able to slide the time back and forth and see how the sequence works.

    However, it gets awkward when you need to make branching decisions or wait for player input. In that case, a node-based FSM like PlayMaker works well. In fact, a lot of devs use PlayMaker for just that.

    That said, if what you're developing works for you, go for it! It reminds me of Adventure Creator's cutscene system. Their cutscenes are called action lists. You might want to take a look at its manual for some ideas. For branching decisions, it has events (called actions) that can either continue to the next event or jump to a different event based on a condition. Many excellent games have been made with Adventure Creator (some using the Dialogue System's Adventure Creator integration, too ;)), so you're certainly on a path that has been successful for others, too.
     
  3. nicmarxp

    nicmarxp

    Joined:
    Dec 3, 2017
    Posts:
    406
    Thanks Tony for the detailed answer! I'm just working on implementing setvariables with +/- and ifvariable, goto, but I have a feeling I'm gonna end up with some complicated logic further down the line. It's hard to predict everything I need in the game, and it's not very fun to rewrite it all.

    I had a hard time figuring out how to make this modular, and instead make it so each eventtype is a scriptable object or class that I can add features to, instead of having one big interaction handler.

    I will definitely take a look at Adventure Creator, thanks again! :)
     
  4. palex-nx

    palex-nx

    Joined:
    Jul 23, 2018
    Posts:
    1,748
    Your collection editor looks nice and professional. But why don't you utilize existing sequencer (aka Timeline) in unity and writing your own instead?
     
  5. Deadcow_

    Deadcow_

    Joined:
    Mar 13, 2014
    Posts:
    133
    Mine is pretty much like yours. It quite simple and lacks of conditional branching (for now). Some of items in the sequence are repeatitive, some will run only once.
    In this example we'll show image, wait for input, hide image, gain xp and loot some items.
    If we'll interact agait all sequence will be repeated except of XP and Container, them are set to run once by default.
    Well... it's actually nothing special about in, don't know why I'm showing this :D

    For variables I usually use ScriptableObjects so it won't be hard to assign through inspector and assign to any other object on scene.

    I saw very similar interactive system but with conditions in this tutorial project

    InteractiveStack2.gif
     
    TonyLi likes this.
  6. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,670
    The advantage of ScriptableObjects is that Unity can serialize subclasses by default. However, since they're separate UnityEngine.Objects, you need to remember to CreateInstance and Destroy them. You can't just leave them for the garbage collector to clean up. But it makes each event subclass much smaller and cleaner. And you can use reflection to pick up new subclasses automatically; you don't need to define a list of them in code.

    If you have a copy of Quest Machine, feel free to take a look at how it's implemented. Quest conditions, actions, and UI content are all sub-ScriptableObjects. Devs can create new subclasses, and Quest Machine's editor automatically adds them to the dropdown list. But you'll see that there's also a fair amount of code to make sure they're instantiated and destroyed properly.

    When it comes to non-branching and non-interactive sequences, I agree. (I personally prefer SLATE to Timeline, but it's the same idea.) Timeline gets awkward when you need to stop and wait for user input or change paths based on a condition. Perhaps one way to get the best of both worlds would be to add an event that can run a Timeline.
     
  7. nicmarxp

    nicmarxp

    Joined:
    Dec 3, 2017
    Posts:
    406
    @palex-nx I haven't looked into it, but I'm performing all kinds of different events, setting variables, and also branching (actually a simple goto) depending on the state of varibles.

    @Deadcow_ That looks neat, how did you set it up so you select from different items? Are those scriptable objects? Right now I have all Eventtypes setup as enums, and the code to execute what events was chosen in one file. See my code below. Each event has pretty simple code, but it seems kinda of cumbersome to edit that file each time I make a new event, and would rather have them in separate classes/files.

    What @TonyLi suggests of automatically adding subclasses and let them get picked up automatically sounds nice, but I'm pretty new to C# and don't know much about reflection.

    I added some parts of my code, and cleaned it up for readability. Anyhow, seting variables, if/goto and endsequence works pretty nice. If anyone wants to look at the code in more, just let me know.

    I'd be happy for more ideas on how to split this up into scriptable objects. It took quite a while to get where I am now though, but refactoring is always good.. :)

    InteractionEvent.cs
    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using Sirenix.OdinInspector;
    4. using UnityEngine;
    5. using UnityEngine.Events;
    6. using UnityEngine.Serialization;
    7.  
    8. namespace BlueGoo.InventorySystem {
    9.     public enum InteractionEventType {
    10.         Speak = 0,
    11.         EventTrigger = 200,
    12.         MoveTransform = 300,
    13.         Inventory = 400,
    14.         Audio = 500,
    15.         ParticleSystem = 600,
    16.         SetVariable = 800,
    17.         IfVariable = 1000,
    18.         EndSequence = 1100,
    19.     }
    20.  
    21.     public enum IfExpression {
    22.         Equals = 0,
    23.         LessThan = 1,
    24.         MoreThan = 2
    25.     }
    26.  
    27.     [Serializable]
    28.     public class InteractionEvent {
    29.         [HideLabel] [SerializeField] public InteractionEventType type;
    30.  
    31.         // I hid some Odin features here, that hides/shows depending on type
    32.         // Speak
    33.         public string text;
    34.        public float duration;
    35.  
    36.         // EventTrigger
    37.        public UnityEvent eventToTrigger;
    38.  
    39.         // Move Transform
    40.         public Transform transformToMove;
    41.         public Transform targetTransform;
    42.         public float moveDuration;
    43.  
    44.         // Particlesystem
    45.         public ParticleSystem particleSystem;
    46.  
    47.         // Audioclip
    48.         public AudioClip audioClip;
    49.  
    50.         // Inventory
    51.        public Item addItem;
    52.  
    53.         // itemStash / vendor
    54.         public ItemStash itemStashVendor;
    55.  
    56.         // Set variable
    57.         public bool setVariableGlobal; // false = local
    58.         public string setVariableName;
    59.         public string setVariableValue;
    60.  
    61.         // If variable statement
    62.         public bool ifUseGlobalVar; // false = local
    63.         public string ifVariableName;
    64.         public IfExpression ifExpression;
    65.         public int ifCompareValue;
    66.         public int ifGotoIndex;
    67.     }
    68. }
    Interaction.cs (only parts of it, to show how I set it up)
    Code (CSharp):
    1.         public void StartSequence() {
    2.             StartCoroutine(nameof(StartSequenceCoroutine));
    3.         }
    4.  
    5.         IEnumerator StartSequenceCoroutine() {
    6.             MasterManager.StateManager.IsInteracting = true;
    7.             bool exitLoop = false;
    8.  
    9.             for (var i = 0; i < sequence.Count; i++) {
    10.                 // If illegal index, end sequence
    11.                 if (sequence[i] == null) {
    12.                     break;
    13.                 }
    14.  
    15.                 var seq = sequence[i];
    16.                 yield return new WaitForSeconds(seq.delay);
    17.  
    18.                 var e = seq.interactionEvent;
    19.  
    20.                 switch (seq.interactionEvent.type) {
    21.                     case InteractionEventType.Speak:
    22.                         StartCoroutine(Speak(speaker, e.text, e.duration));
    23.                         break;
    24.  
    25.                     case InteractionEventType.ParticleSystem:
    26.                         e.particleSystem.Play();
    27.                         break;
    28.  
    29.                     case InteractionEventType.EventTrigger:
    30.                         e.eventToTrigger?.Invoke();
    31.                         break;
    32.  
    33.                     case InteractionEventType.MoveTransform:
    34.                         break;
    35.  
     
  8. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,514
    We use a system where each event in the sequence is its own Component. We have things to display messages, popup quips, play animations and sfx, and other stuff. Here's a sequence here:

    (sorry, I did not intend for the music to be on there)

    And the results are:


    Note for translation stuff, the i_DisplayMessage and what not can actually be backed by text files and id's pointing at the spot in the text file the text is at. That way we can cleanly translate that. We don't actually use them at this point, but hope to be when we wrap up ep3 and start porting to console.

    And an image so it's not video, this specific dialog component allows pairing voice audio with the text:
    DialogCutscene.png
     
    Last edited: Mar 28, 2019
    TonyLi likes this.
  9. nicmarxp

    nicmarxp

    Joined:
    Dec 3, 2017
    Posts:
    406
    Hehe, nice music. The video was extremely blurry though. So you have the sequence as game objects?

    What I find difficult with a modular setup, is to have completely different parameters for each, and also I hade problems selecting Scriptable object assets from a dropdown, I had to use the popup window, which I think is slightly annoying. Maybe there is a workaround for that, or I misunderstood something..

    Thanks, it's nice to see how you all put things together!
     
  10. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,670
    You have to write a custom editor for that. Find all of the ScriptableObject assets of a specific type:
    Code (csharp):
    1. var guids = AssetDatabase.FindAssets("t:" + type.Name);
    (Call AssetDatabase.GUIDToAssetPath to get the filename from the GUID.)

    You can put the names into an array (maybe sort them alphabetically first) and show them using EditorGUI.Popup -- or GenericMenu if you're using a ReorderableList.

    If you're looking for types and not assets, you can use reflection to get a list of all subclasses:
    Code (csharp):
    1. var subclasses = (from domainAssembly in AppDomain.CurrentDomain.GetAssemblies()
    2.     from assemblyType in domainAssembly.GetTypes()
    3.     where type.IsClass && !type.IsAbstract && type.IsSubclassOf(typeof(T))
    4.     select T).ToArray();
    When the user chooses one, you can create an object of that type.

    The nice thing about lordofduct's component- and GameObject-based approach is that you can avoid all of that. Just add components using the regular Unity editor features. (Although you can certainly add custom helper code to make it even more convenient.)
     
  11. Deadcow_

    Deadcow_

    Joined:
    Mar 13, 2014
    Posts:
    133
    Yes, I prefer SO over enums in almost every possible cases. I may imagine how beneficial SOs may be in your scenario.
    In your case you will be able to avoid cumbersome switch statement, separate individual event logic and some useful settings along with SO objects. Like, you may have Audio event and make two objects for Audio2d and Audio3d with a single bool parameter.
    And since all Events will be derived from one Event SO base class with IEnumerator Fire() function you may imagine some benefits out of that
     
  12. nicmarxp

    nicmarxp

    Joined:
    Dec 3, 2017
    Posts:
    406
    @Deadcow_ Thanks so much for pointing me to Unitys Adventure Game Tutorial. I've now watched all of it, and I'm studying the code now. I think it's very smart, and very modular.

    However, I'm not quite happy with how the conditions work. In my current setup, I have local variables, like how many times a button on a machine has been pressed. And I have global variables, pretty much like the "All Conditions" assets in the tutorial.

    I have an action (Would be called a reaction in their code) that checks a local variable, and if it's for example 3, it would branch off to another part (which I think would work with another reaction to jump back and forth)

    However, their Conditions only allow true/false, so I'm looking into how to expand it, or simply modify the condition to allow a number, and then check for that. But I guess it would be useful to have more conditions types, like "IfPlayerHasItem", and checking the inventory, instead of storing that as a bool in a SO asset.

    I find it a little disheartening to break down everything I got and convert it into this, but I just know that it will be worth it in a few weeks time, when I've added more conditions, reactions and stuff, and don't have to have everything in 2-3 huge files. :)

    Let me know if you find out any great ideas about branching, and I'll let you know if I figure it out based on this tutorial. It was hugely advanced on how to create editor scripts, and I think some of it could be skipped by just using Odin Inspector which is great.

    Thanks again for the inspiration!

    EDIT: I posted a question in the official thread for the adventure game tutorial, and hopefully someone will pick it up.. :)
     
    Last edited: Apr 7, 2019
  13. nicmarxp

    nicmarxp

    Joined:
    Dec 3, 2017
    Posts:
    406
    @Deadcow_ hey again, after watching several hours of the tutorials, and also studied the code a lot, I've come to the conclusion to build my own anyways, but base it on the same principles Interaction-Conditions-Reactions. The editor scripts in the tutorials were so BIG and complicated, and I'm not sure I need something like that, even if it does what it's supposed to very good.

    I wanted to extend on the conditions, and got stumped on the complexity of the editors.

    However, after drawing all the editors out, I realized that what I want is very similar to what you showed in the GIF above. Except that Stack seems to be my ReactionCollection. Is that correct?

    The basic problem is that I want to click "Add condition" to a ConditionCollection (and ReactionCollection), and pick from a list of ScriptableObjects, and then see the properties of that particular child object. I guess it needs to add an instance of the SO in the ConditionCollection (Stack in your case?). Where is that saved really?

    Also is it correct that I need to do an editor script for each subclass, like ItemCondition, StateCondition, etc ?

    I have Odin Inspector, and I think that will help, but if you could share some code, either here or in a private message, that will be very appreciated.

    Thanks in advance!

    PS: Your game The Final Station looks awesome, very cool art-style. :) (But you misspelled it in your signature..)
     
  14. Deadcow_

    Deadcow_

    Joined:
    Mar 13, 2014
    Posts:
    133
    Oh my god :D thanks for telling me that.

    It seems that my InteractiveStack and your ReactionCollection is conceptionally the same things. I just got to find better name for it :)

    Although I didn't implemented branching in my system yet, I've got some ideas.
    First of all, the idea that every reaction sequence is separate GO, like in Adventure Tutorial seems right, except instead of GO it may have some benefits to use ScriptableObjects (like one interactive with one condition ends up with three GO or SO).
    In my case every "interactive item", like "Container" or "Note Image" are actually MonoBehaviours hidden in inspector on the same GO (with hideFlags), but they may be simple serialized structs as well. This tiny checkbox on the right bottom is to hide/show this MB scripts in the inspector for debug purposes.
    I don't think it's possible to use SO for InteractiveItems here because every InteractiveItem should have some unique parameters.

    So, if in the middle of the sequence you need some conditional branching, you'll end up with three ReactionCollections. The first one (default) will end at the "BranchReaction". And this branch item will contain some condition and references to two other ReactionCollections, one will run if condition is true and another for false.
    Here for conditions UnityAction might be handy. It'll allow you to hook up any method with return bool and pass some parameters. Like ItemType (enum or SO) and int Amount in your case for inventory check. You may extend your ReactionCollection to handle any conditions, like inventory checks and some global checks like PlayerHP or Score (although in my current game every thing like XP, HP, HealthPotion, GoldKey or even LocationR16IsVisited knowledge is Items, some are InventoryItem, some StatItem, and KnowledgeItem but all handled through the same systems and made with ScriptableObjects).


    About editor scripts for every InteractiveItem, yes, in my case I came up with pretty ugly solution, where even with single property I've got to write custom inspector code instead of DefaultInspector.
    I use my implementation of ReorderableCollection where you may add CustomDrawer and CustomDrawerHeight for every item in collection. Every item in stack derived from InteractiveItem:
    Code (CSharp):
    1.  
    2. #if UNITY_EDITOR
    3.         public virtual int DrawerHeight => 16;
    4.  
    5.         public abstract void DrawInspector(Rect rect, UnityEditor.SerializedObject serializedObject);
    6. #endif
    7.  
    and that's how InteractiveAnimation handles its drawing :
    Code (CSharp):
    1.  
    2. #if UNITY_EDITOR
    3.         public override void DrawInspector(Rect rect, UnityEditor.SerializedObject serializedObject)
    4.         {
    5.             UnityEditor.EditorGUI.PropertyField(rect, serializedObject.FindProperty("Animation"), new GUIContent("Play Animation: "));
    6.             serializedObject.ApplyModifiedProperties();
    7.         }
    8. #endif
    9.  
    and then I bind this ReorderableCollection.CustomDrawer with specific InteractiveItem DrawInspector method inside of InteractiveStackDrawer...

    I hope it all makes sense :) If I missunderstood something in your questions please clarify it, I'm not too good in English as you may see already :D
     
  15. nicmarxp

    nicmarxp

    Joined:
    Dec 3, 2017
    Posts:
    406