Search Unity

Cleaner way to hook data-driven content to in-game triggers

Discussion in 'Scripting' started by Sendatsu_Yoshimitsu, Jul 1, 2018.

  1. Sendatsu_Yoshimitsu

    Sendatsu_Yoshimitsu

    Joined:
    May 19, 2014
    Posts:
    691
    All of my game's quests and story stuff is written in an external editor, imported into Unity as JSON files, and parsed as needed. For stuff like cutscenes this is all it takes- characters retain a cutscene ID, and talking to them just retrieves and displays the corresponding content. In the case of quests, however, I'll often want to spawn very specific event triggers into the game world, like plot items which trigger a cutscene when interacted with, or a group of characters who do something when approached.

    Right now, I handle this manually: every cutscene and piece of story logic in each quest has a list of InstantiatedHook structs, each of which specifies a location in the world, a prefab to spawn at that location, and the type of trigger (interaction/proximity/destruction/etc) to hook up. At runtime, whenever a new area is loaded in it iterates through every active quest node, retrieves every <location/prefab/trigger> trio, and checks if any of its internal spawn points match the designated location. If they do, the desired model and trigger are spawned and configured.

    This works, but it has several key architectural issues that I'm not happy with:

    1) locations, prefabs, and triggers are all specified outside of unity via string IDs, which raises the possibility of typos. This can be mitigated with an in-engine debug tool that scans every quest and validates/sanitizes all of the IDs, but the core workflow will require writers to work with a giant table of valid location and prefab IDs in-hand.

    2) Iterating through each node of every active quest whenever the current level changes sucks. Our worst case scenario involves about 30 main quests + side content being active at one time, each quest has potentially 5-10 nodes active at one time, and the average level has between 100-200 potential spawn points. That means that 300 times per level load, I'm going to be iterating down a list of 100-200 spawn points and comparing string IDs to see if they match. I know it's not that big of a deal on modern CPUs, especially since it'll happen behind a loading screen, but 60,000 string-string comparisons is going to generate a ton of garbage and feels like a brute force way to solve the problem.


    The thing is, as inelegant and awkward as this current workflow is, I'm honestly not sure if there's a pattern that would clean it up; the string comparisons have to occur at some point, since every quest needs to check if it has any content for the new level, and running the checks behind a loading screen where no hitching or lag will be noticeable feels like a best-case scenario. The reference table is likewise clunky, but writers will always need some kind of reference to where they can instantiate content into the world.

    So, am I missing anything huge? This feels like a case of "sucky, but acceptable," but I can't help but wonder if I'm using a hammer to drive screws.
     
  2. Fajlworks

    Fajlworks

    Joined:
    Sep 8, 2014
    Posts:
    344
    Your question really intrigued me and got me thinking. If the string ID specified outside of unity is always unique, you could create an in-engine tool that assigns a unique integer to every string ID. That way during run time you can compare ints instead of strings. Once in editor or at runtime, IDs won't change I assume? It might not be the most elegant solution as it quite hard for me to visualize your node approach, but it I guess it would be much faster than comparing strings?
     
  3. Sendatsu_Yoshimitsu

    Sendatsu_Yoshimitsu

    Joined:
    May 19, 2014
    Posts:
    691
    Hmm yeah, I could hash them to ints... I've never implemented hashing before, but I'm betting that you're absolutely right about it generating less overhead than string-string comparisons.
     
  4. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    916
    1) If the number of assets grows too large your could always use Asset Labels and filter down to just the assets they only want to look at. For example you could give a quest asset a "Quest", "Side Quest", "Repeatable", and "IQuestable" AssetLabel. Then your writer/designers can quickly filter out the Project panel to show only the repeatable side quests, quickly find the quest they are looking for, grab the id, and then just plant it in the json.

    As long as the writers and designers have access to the project (which they always should while working on it) they should have an up-to-date reference tool they can use to hook things up

    2) What you could do is just setup a partitioning system for your spawn points. Have your SpawnPoint component store a private static IDictionary<string (for Tag/Name/ID/whatever), IList<SpawnPoint>> AllSpawns. When the SpawnPoint is created it registers itself to the lookup, when destroyed it unregisters.

    This allows things like your quest nodes to focus only on the nodes that matter to them. they could call a function like public static IEnumerable<Spawnpoints> GetAllSpawnPointsByTag(string tag) (or ByName/ByID/ByWhatever you are categorizing them with) that way the quest can iterate only the spawns that matter while the Spawnpoints can keep their partition encapsulated. plus this significantly reduces the number of string compares
     
  5. teutonicus

    teutonicus

    Joined:
    Jul 4, 2012
    Posts:
    71
    Sounds like you're just missing an asset build/compile step. Whenever building for QA/profile/release, convert all of your json data to a runtime friendly format with all string ids etc. hashed/stripped out. Editor/development builds can follow the same build step or just pass through to your current json methods if asset build time becomes prohibitive. Designers get to use nice strings and you get to reduce garbage collection, runtime json parsing and string comparisons. Everybody wins :)