Forum rules - please read before posting.

Storing and processing Actor-specific data

What I want to do:

> Store, and process, actor-specific data (such as health, occupation, etc), in such a way that it's naturally included in AC's Save/Load system, or, if that's not possible, in such a way that it can be saved at the same time as AC executes a Save (and, of course reloaded when AC executes a Load)

Currently I'm storing this data in the AC Global variable system. It's just about manageable with only a few actors, and just a few types of data, but even then the variable system gets clogged up very quickly, and it's clearly sub-optimal to store a set of variables called, e.g. Actor1_Health, Actor2_Health, Actor3_Health, etc etc. That's why god invented arrays

But, no arrays in AC, so I need to come up with some other mechanism for handling such data

Now, I know how to script a system to store the data under normal, non AC, circumstances, and, I know (theoretically at any rate) how to use AC scripting to keep such data synchronised with the AC Save/Load system. But, that doesn't really get me any further forward, because in order to do that, I have to pre-define the variables I'm going to use in the AC Global variable system, so... it's still going to get clogged up very fast

So, what to do? I'm sure other users must have come up against this problem before. In AGS you can define arrays within its scripting system, and that data automatically gets included in the AGS Save/Load system, but I can't quite get a handle on how to achieve the same thing in AC, so any hints or tips would be much appreciated

Comments

  • edited May 2018
    As a small p.s. one solution that I've toyed with is to encode each set of data into its own AC string variable, so, for example, "ActorHealth", would be a string variable containing something like the following (assuming in this example, that I had five actors in the game, with healths of 100, 90, 1, 110 and 100, and that the string delimiter I was going to use was ";"):

    "100;090;001;110;100;"

    It seems a little bit of an awkward, and potentially error-prone solution, but it does have the big advantage of keeping the number of AC global variables down, and would mean that the data would be automatically included in the AC Save System

    I actually wrote a script a while back to encode and decode the data in this kind of format, before abandoning the idea, but I wonder if this is potentially the best solution after all?


  • edited May 2018
    @HarryOminous Yeah, I've tried parsing data into regular old strings in the past too, like: 
  • void UpdateAcGlobal(Date date)
            {
                //updates only specified date
                string data = date.MonthDay + "," +
                              (int)date.WeekDay + "," +
                              (int)date.Month + "," +
                              date.Year + "," +
                              (int)date.MorningWeather + "," +
                              (int)date.NoonWeather + "," +
                              (int)date.AfternoongWeather + "," +
                              (int)date.EveningWeather + "," +
                              (int)date.LateNightWeather + "," +
                              (int)date.TimeOfDay;

                if (Application.isPlaying)
                {
                    AC.GlobalVariables.SetStringValue(date.AcGvarId, data);
                }
                else
                {
                    AC.VariablesManager varManager = AC.AdvGame.GetReferences().variablesManager;
                    AC.GVar var = varManager.GetVariable(date.AcGvarId);

                    if (var.type == VariableType.String)
                    {
                        var.textVal = data;
                    }
                    else
                    {
                        Debug.LogWarning("Variable was not a string! aborting... " + gameObject.name);
                    }
                }
            }

            void LoadFromAcGlobal(Date date)
            {
                //updates only specified date
                string str = "";
                if (Application.isPlaying)
                {
                    str = AC.GlobalVariables.GetStringValue(date.AcGvarId);
                }
                else
                {
                    AC.VariablesManager varManager = AC.AdvGame.GetReferences().variablesManager;
                    AC.GVar var = varManager.GetVariable(date.AcGvarId);

                    if (var.type == VariableType.String)
                    {
                        str = var.textVal;
                    }
                    else
                    {
                        Debug.LogWarning("Variable was not a string! aborting... " + gameObject.name);
                        return;
                    }
                }


                if (String.IsNullOrEmpty(str))
                    return;
                List<int> dateInfo = new List<int>();

                foreach (var s in str.Split(','))
                {
                    int num;
                    if (int.TryParse(s, out num))
                        dateInfo.Add(num);
                }

                date.MonthDay = dateInfo[0];
                date.WeekDay = (WeekDays)dateInfo[1];
                date.Month = (Months)dateInfo[2];
                date.Year = dateInfo[3];

                date.MorningWeather = (WeatherTypes)dateInfo[4];
                date.NoonWeather = (WeatherTypes)dateInfo[5];
                date.AfternoongWeather = (WeatherTypes)dateInfo[6];
                date.EveningWeather = (WeatherTypes)dateInfo[7];
                date.LateNightWeather = (WeatherTypes)dateInfo[8];

                date.TimeOfDay = (TimesOfDay)dateInfo[9];
            }
  • edited May 2018
    That can work nicely in some cases, but you have to be careful not to overwrite the data in the variables manager. But, if the data gets longer and longer it's probably better to do a binary serialization.
  • using System;
    using System.IO;
    using UnityEngine;
    using System.Runtime.Serialization.Formatters.Binary;
    /*
     * Saves any class.
     * 
     * Example usage:
     * Serializer.Save<ExampleClass>(filenameWithExtension, exampleClass);
     * ExampleClass exampleClass = Serializer.Load<ExampleClass>(filenameWithExtension));
     */
        public class Serializer
        {
            public static T Load<T>(string filename) where T : class
            {
                if (File.Exists(filename))
                {
                    try
                    {
                        using (Stream stream = File.OpenRead(filename))
                        {
                            BinaryFormatter formatter = new BinaryFormatter();
                            return formatter.Deserialize(stream) as T;
                        }
                    }
                    catch (Exception e)
                    {
                        Debug.Log(e.Message);
                    }
                }
                return default(T);
            }

            public static void Save<T>(string filename, T data) where T : class
            {
                using (Stream stream = File.OpenWrite(filename))
                {
                    BinaryFormatter formatter = new BinaryFormatter();
                    formatter.Serialize(stream, data);
                }
            }
        }
  • edited May 2018
    That's a basic Serializer class (it will create a save file for you). It works on Generics so it can take any serializable class you throw at it (as long as the types are serializable), only downside is that you have to make sure to use AC's save event's when saving your own data to stay synchronized with AC...

    There's also Json serialization, but I rather like the method above.
  • Though arrays aren't available as a Variable type, there are a few instances of multiple sets of data being stored (e.g. collected journal pages), and using a separator to store such data as a single string is exactly the same technique.

    Two tutorials are available on custom save data:
  • Rght, using AC's ISave Interface is rather effective in most cases. I haven't really used string parsing in a long, long time though, but I thought showing an example might be helpful to someone, haha. Anyway, I do prefer Binary Serialization when the data get's really complex and you need to save at other times other than just the predetermined AC's save/load...
  • edited May 2018
    Thanks for the feedback, both

    I've had a need for this facility pretty much since day one, but I've put off doing anything significant about it in case I ended up heading down a blind alley (and also because I'm a lazy boy at heart)

    Sounds like I wasn't too far off the right track with the idea of storing each dataset as a delimited string, but saving the data independently via binary serialization has some attraction too

    Pros and cons for both approaches, I suspect, but the only way to find out will be to knuckle down and actually try and implement one, or the other, (or both)

    @Alverik thanks for those code snippets. I'll probably use them as a template for my experiments. I may report back in this thread later, depending on how things progress
  • edited May 2018
    @Alverik: I've actually just updated the global data tutorial to rely on custom events as opposed to the ISave interface method.  Though it'll remain, I think the event system now supersedes it in terms of extending functionality.

    @HarryOminous: As the update tutorial describes, the best way to hook into AC's save system is to hook into the OnBeforeSaving / OnAfterLoading custom events.
  • edited May 2018
    Thanks for the advice and suggestions guys

    Once I was confident I wasn't going down a blind alley, it didn't actually take that long to get something up and running

    In the end I settled on saving each array as a delimited string, which has the advantage that you can see (at least part of) the data dynamically in the Variables Manager, which may prove helpful in debugging. It also means that the data is naturally included in AC's Save/Load system

    What proved very useful was Alverik's str.Split(',') trick, which I used to replace some horribly clunky code I'd written earlier when I was first thinking about this approach

    Basically what I do is initialise the string "arrays" when the game starts, and maintain the data in a set of real arrays, in parallel. I rebuild the string equivalent whenever the real internal array data changes, and I rebuild the real internal arrays from the string data whenever an AC Load occurs

    All I need to do now is write some ActionLists, one to Set the data for each array/element, and one to Check the data. Writing user ActionLists is well documented (as is everything in AC) and I've already written a couple of small ones for other functions, so I'm not expecting (famous last words) that to prove problematic. I'll use ActionVarCheck and ActionVarSet as templates

    The only thing that's delaying me on that score is trying to decide whether it would be better to create one giant whack-off version of each to handle all the different arrays, driven by enums to control which array to process - or whether it would be better to create one ActionList per array. I suspect the former is the correct way to do it, even though it will probably lead to large, indigestible, chunks of code

    Thanks again for the feedback
  • well, i left a long, rambling "thanks for the feedback, and this is how i implementd it" post, but as i was making a few last-minute tweaks (being a bit OC about such matters), *poof* it disappeared into some kind of "awaiting approval" limbo

    so, hopefully the post hasn't gone for good, but if it has: "thanks for the feedback, and this is how i implemented it" (um, yanno, with the strings, and the parallel arrays, and the onafterloading events, and suchlike)
  • edited May 2018
    Since the last update, the AC forum marks comments that have been edited about 3 times as Spam, happened to me before. Wait until Chris approves your post.
  • Sorry for the trouble - I've unflagged it and updated your account.
  • No problem, I used to run a forum once myself, so I know that spam's a perennial headache. I'll just have to remember to limit my forum post edits in future

    As to the matter in hand, I settled on creating one single "Array Element Check" and one single "Array Element Set" ActionList, which allow the user (me) to select by Array name and Element ID (both currently driven by enums), and then check the values in the array, or update them, depending on the ActionList. They're pretty much written now. Just got to battle test them under real game conditions

    The only small drawback to the delimited strings approach is that I can only have one array-string per dataset (i.e. I've got one for ActorDialoguePoint encoded as numeric data, one for ActorName encoded as string data, one for ActorNameKnown encoded as boolean data, even though they're all "Actor" arrays with the same number of entries in), and it needs a non-trivial amount of manual scripting in order to add any new ones, as I'll surely need to do. But, I can live with that if it gives me the ability to process array data and keep the AC Global Variable list manageable

    Now, back to the fun stuff...


Sign In or Register to comment.

Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Welcome to the official forum for Adventure Creator.