Search Unity

Best practice when loading something that requires a reference of something you haven't load yet.

Discussion in 'Scripting' started by Redden44, Feb 18, 2020.

  1. Redden44

    Redden44

    Joined:
    Nov 15, 2014
    Posts:
    159
    Hello, I wrote a threat meter that contains a list of characters and their threat value. Every character has got one and when I save a character I save the threat meter's list as well (all is done via Xml).

    When I load a character, I need to load the threat meter data as well, problem is to create the threat meter's list I need a reference to the enemy, which may or may not exist in the world already.

    In this case for each listed enemy, I'm saving its ID (int) and threat value (float), should I create a list of data and initialize that data after the loading is done? Should I do that in the same frame? If I do that in the next frame, the Update functions will kick in and hell will break loose..

    I have the same problem for a character's state machine with an AttackState that requires the enemy reference and when I load the character, the enemy doesn't exist yet.

    What do you usually do in this case, i.e. when you need a reference of something that doesn't exist yet and of course you can't load that thing before the one that requires it?

    This is the function I'm using to load the threat meter data and at the moment all enemies will be null because they don't exist yet.

    Code (CSharp):
    1.  
    2.     public virtual void ReadXml(XmlReader reader)
    3.     {
    4.         XmlReader inner = reader.ReadSubtree();
    5.         while (inner.Read())
    6.         {
    7.             switch (inner.Name)
    8.             {
    9.                 case "Enemy":
    10.                     int id = int.Parse(inner.GetAttribute("id"));
    11.                     float threat = float.Parse(inner.GetAttribute("threat"));
    12.                     ITarget enemy = Zone.Current.CharacterManager.GetCharacterWithID(id);
    13.                     if (enemy != null)
    14.                     {
    15.                         AddEnemy(enemy, threat);
    16.                     }
    17.                     break;
    18.             }
    19.         }
    20.         inner.Close();
    21.     }
    Thanks.
     
    Last edited: Feb 19, 2020
  2. unit_dev123

    unit_dev123

    Joined:
    Feb 10, 2020
    Posts:
    989
    U can use json file xml is quiet old technology now.
     
  3. dgoyette

    dgoyette

    Joined:
    Jul 1, 2016
    Posts:
    4,196
    You might want to edit your post and replace "thread" with "threat". You typed it wrong many times, and it makes your post a little bit confusing.

    So, just to make sure I understand, it sounds like you're saving the state of a battle in the middle of the fight, so that you can restore the battle to its original state when the player reloads the game? And you're running into issues with all the interdependencies between enemies and players?

    It seems like your basic approach is reasonable. Anything you serialize should have an ID of some sort. When loading the data, you probably need to instantiate all of the objects in one pass (i.e., create all the players and enemies) then take a second pass to link up all the dependencies based on the IDs.

    One way to think of this is the way Unity's Awake and Start functions work. During "Awake", you generally don't assume that other objects in the scene are fully ready to be used yet, so you set up your own internal data. When Start() is called, you assume that all dependencies should be available in the scene. I'm not saying you should literally use Awake and Start in some way for this, but just to recognize that the deserialization probably needs multiple steps.

    Did I understand the issue? Or is something else going on?

    The serialization technology doesn't seem to have anything to do with the problem he's working on. How would switching to JSON help?
     
    Bisoncraft likes this.
  4. unit_dev123

    unit_dev123

    Joined:
    Feb 10, 2020
    Posts:
    989
    Best use recent tech rather than antiquated tech :)
     
  5. kdgalla

    kdgalla

    Joined:
    Mar 15, 2013
    Posts:
    4,639
    Are you sure that you actually need to save the threat level? Is the threat level just a rating of how dangerous each character is? I don't know how your game works, but as long as you save the state of each character, it sounds like the threat level is something that can be reconstructed once you load that information.
     
  6. dgoyette

    dgoyette

    Joined:
    Jul 1, 2016
    Posts:
    4,196
    We'll see what the OP says, but I interpreted it as each monster has a dynamic list of which character is the greatest threat, based on which character has damaged it most. Kind of like how MMORPGs have an aggro list, which players can go up and down within. Thus, it's totally dynamic, changes every turn of combat, etc. But we'll see.
     
  7. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    2,836
    In RPG-style battle games, "threat" often refers to a variable in the mind of the AI that denotes how angry it is with you (which is then used by the AI to determine who to attack). This is usually a path-dependent variable that can't be recalculated from the current state of the battle, because it depends on things like how often you've attacked the AI in the past. (It could probably be recalculated from a replay of the whole battle, but not from one time-slice.)

    Saving the internal state of AI agents is often a complex problem, and it's easy to overlook when you are subconsciously thinking of them like players rather than like game systems.
     
  8. Redden44

    Redden44

    Joined:
    Nov 15, 2014
    Posts:
    159

    Sorry, I edited the post and hopefully fixed all the mistakes, English isn't my first language, apologies.

    Yes, it's a base building game and I need to save the game at any given moment; so if the save kicks in during a battle, I need to be able to load the scene exactly as it was during the save.

    The threat meter is going to allow each character to have a list of known enemies ordered according to their threat, which at the moment is just the damage they deal to a character. When a character is hit, the meter increases the enemy's threat and the list is reordered; if the most threatening enemy changes, a callbacks kicks in and the character may or may not switch target according to its AI, task, etc.

    The threat meter has got a Dictionary<ITarget, float> where ITarget is an interface to track an enemy and the float is its threat value. I could switch to a Dictionary<int, float> where int is the enemy's ID, but it won't completely solve the problem and every time I need an enemy I will have to convert the ID into an enemy reference.
    Another solution could be to load the list in a secondary Dictionary<int, float> and when the loading is done, go back to each character's threat meter and elaborate that data, checking each entry of the secondary Dictionary and if there is an enemy for the given ID, I could create an entry in the primary Dictionary.
    For this specific case I could also save all the threat meters only after all the characters are saved, this way when I load and initialize the meters, all characters would be already in the game, but as I said it works only for this case.

    I have the same problem with the AttackState because it requires a reference of a target. If I load a character before I load its target, the target won't exist. In this case I think I can create a helper State which during the first frame (i.e. after the loading is done) will check for the enemy, if the enemy exists, it will just end itself and the StateMachine will transit to the next state (i.e. the AttackState). If the enemy doesn't exists (due to a bug or because a mod was unloaded), then the HelperState will simply reset the character State.

    A solution could be not saving during combat, but if a battle/siege takes 20 minutes or more and something happens, a crash maybe, the player will lose a lot of playtime. It seems a dumb solution.

    I have a few solutions, problem is I would like to pick "the correct way" (if there is one), not just apply a different fix each time the problem rises, thanks!
     
    Last edited: Feb 19, 2020
  9. Redden44

    Redden44

    Joined:
    Nov 15, 2014
    Posts:
    159
    Yes I need to save it to reconstruct the battle exactly as it was before.

    For instance, let's say a character was hit by an enemy and he couldn't return fire because he was doing a critical task (e.g. he was running away to repair a machine or rescue another character), the threat meter allows that character to know that there is that enemy and it needs to be dealt with once he's done with his current task. If you don't save and load the threat meter, that character won't know that he saw an enemy and the enemy attacked him and he needs to retaliate once he's done with his current task.

    I could save all the threat meters after I save all the characters and load the threat meters after I load all the characters, this way all enemies would exist when I look for their references, but I have the same problem with a character's AttackState which needs a reference to a target. I guess I could save the States after the characters too (not sure at the moment), but is it really a good practice or there is another solution? Thanks!
     
    Last edited: Feb 19, 2020
  10. Redden44

    Redden44

    Joined:
    Nov 15, 2014
    Posts:
    159
    This is the threat meter script (it's very basic and I didn't tested it yet), I don't know if it can help.

    At the moment the WriteXml() and ReadXml() functions are called from the Character class, which is called from the CharacterManager (which contains a master list of all characters) when the game needs to save/load data.

    So a Character will save his class, ID, name, attributes, skills, stats, equipment, etc, State and Threat Meter and then he will load them in the same order. Now the State and the Threat Meter requires references to other characters (enemies) which aren't loaded yet and this is the problem.
    I posted some solutions in one of my previous posts, but to me they look just "patches" which an inexperienced scripted like me can come up with; my question is if there are any "best practices " normally used to avoid this kind of problems? For instance maybe I should to load the data in a different way compared to what I'm doing, like save/load all the data that requires no references first and then do a second pass for the rest; or maybe I should load the data and then do a second pass to initialize it? Thanks!

    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using System.Linq;
    5. using System.Xml;
    6.  
    7. public class ThreatMeter
    8. {
    9.     // Character the threat meter belongs to.
    10.     private Character character;
    11.  
    12.     // List of enemies and their threat level.
    13.     private Dictionary<ITarget, float> enemyToThreatMap;
    14.     public int EnemyCount { get { return enemyToThreatMap.Count; } }
    15.  
    16.     // Tells the character that the most threatening enemy is changed.
    17.     public event Action<ITarget> cbMostThreateningEnemyChanged;
    18.  
    19.  
    20.     // Constructor.
    21.     public ThreatMeter(Character character)
    22.     {
    23.         this.character = character;
    24.         this.character.Health.cbHit += OnCharacterHit;
    25.         this.character.Health.cbDestroyed += (c) => enemyToThreatMap = null;
    26.         enemyToThreatMap = new Dictionary<ITarget, float>();
    27.     }
    28.  
    29.     // Called when a character is hit.
    30.     private void OnCharacterHit(ITarget character, ITarget damageSource, float damage)
    31.     {
    32.         if (damageSource == null)
    33.         {
    34.             return;
    35.         }
    36.  
    37.         if (enemyToThreatMap.ContainsKey(damageSource) == false)
    38.         {
    39.             AddEnemy(damageSource, damage);
    40.         }
    41.         else
    42.         {
    43.             IncreaseThreat(damageSource, damage);
    44.         }
    45.     }
    46.  
    47.     // Adds an enemy.
    48.     public void AddEnemy(ITarget enemy, float damage)
    49.     {
    50.         enemyToThreatMap[enemy] = damage;
    51.         enemy.Health.cbDestroyed += RemoveEnemy;
    52.         // TODO: enemy.Health.cbUnconscious += RemoveEnemy;
    53.         // TODO: enemy.cbFledFromZone += RemoveEnemy;
    54.         if (damage > 0)
    55.         {
    56.             ReevaluateEnemyOrder();
    57.         }
    58.     }
    59.  
    60.     // Increases the threat.
    61.     private void IncreaseThreat(ITarget damageSource, float damage)
    62.     {
    63.         enemyToThreatMap[damageSource] += damage;
    64.         if (enemyToThreatMap.FirstOrDefault().Key != damageSource)
    65.         {
    66.             ReevaluateEnemyOrder();
    67.         }
    68.     }
    69.  
    70.     // Reorder the list.
    71.     private void ReevaluateEnemyOrder()
    72.     {
    73.         ITarget topEnemy = enemyToThreatMap.First().Key;
    74.         enemyToThreatMap.OrderByDescending(x => x.Value);
    75.         if (topEnemy != enemyToThreatMap.First().Key)
    76.         {
    77.             topEnemy = enemyToThreatMap.First().Key;
    78.             if (topEnemy != null && cbMostThreateningEnemyChanged != null)
    79.             {
    80.                 cbMostThreateningEnemyChanged(topEnemy);
    81.             }
    82.         }
    83.     }
    84.  
    85.     // Removes an enemy.
    86.     public void RemoveEnemy(ITarget enemy)
    87.     {
    88.         if (enemy != null && enemyToThreatMap.ContainsKey(enemy))
    89.         {
    90.             enemyToThreatMap.Remove(enemy);
    91.         }
    92.     }
    93.  
    94.     // Return the most threatening enemy.
    95.     public ITarget GetMostThreateningEnemy()
    96.     {
    97.         // FIXME: add more checks.
    98.         return enemyToThreatMap.FirstOrDefault(x => x.Key.Health.IsAlive).Key;
    99.     }
    100.  
    101.     #region Saving and Loading
    102.  
    103.     // Save data, called by Character class.
    104.     public virtual void WriteXml(XmlWriter writer)
    105.     {
    106.         foreach (KeyValuePair<ITarget, float> pair in enemyToThreatMap)
    107.         {
    108.             writer.WriteStartElement("Enemy");
    109.             writer.WriteAttributeString("id", pair.Key.ID.ToString());
    110.             writer.WriteAttributeString("threat", pair.Value.ToString());
    111.             writer.WriteEndElement();
    112.         }
    113.     }
    114.  
    115.     // Load data, called by Character class.
    116.     public virtual void ReadXml(XmlReader reader)
    117.     {
    118.         XmlReader inner = reader.ReadSubtree();
    119.         while (inner.Read())
    120.         {
    121.             switch (inner.Name)
    122.             {
    123.                 case "Enemy":
    124.                     int id = int.Parse(inner.GetAttribute("id"));
    125.                     float threat = float.Parse(inner.GetAttribute("threat"));
    126.                     ITarget enemy = Zone.Current.CharacterManager.GetCharacterWithID(id);
    127.                     if (enemy != null)
    128.                     {
    129.                         AddEnemy(enemy, threat);
    130.                     }
    131.                     break;
    132.             }
    133.         }
    134.         inner.Close();
    135.     }
    136.  
    137.     #endregion
    138. }
     
    Last edited: Feb 19, 2020
  11. Bisoncraft

    Bisoncraft

    Joined:
    Feb 5, 2016
    Posts:
    37
    I completely agree with dgoyette says above.

    I guess your problem is that you don't see exactly how to make sure that scripts are executed in the right order? I didn't see here which code triggers loading objects; ie, which class and method calls ThreatMeter.ReadXML.

    What I've done in my own code and which is similar to what dgoyette describes is that I've got two differents methods:
    - ReadXML (like you) - loading the object and also the IDs or related objects (enemies, etc)
    - RefreshLinks - this method will load the actual reference (Unit, etc) based on the ID - you get the idea.

    So wherever I load my objects, I make sure that:
    - all objects are loaded (foreach XMLNode in File){ReadXML (XMLNode);}
    - then all references are refreshed (foreach Unit in ListOfUnits ) {RefreshLinks();}
    I have no problem with Update because it all happens in one frame.

    I think it should work, in your case.
     
  12. Redden44

    Redden44

    Joined:
    Nov 15, 2014
    Posts:
    159
    Basically it works like a chain, when a game is saved a WorldController script tells all managers and controllers to save their data, so in this case it calls CharacterManager.WriteXml(). Now the CharacterManager has got a master list of all characters and tells to each one of them to save his data calling Character.WriteXml(). Each character saves his own data (id, name, attributes, etc) and then tells his systems to save their data, for instance ThreatMeter.WriteXml().

    So WorldController => CharacterManager.WriteXml() => Character.WriteXml() => ThreatMeter.WriteXml().

    The loading works exactly the same way and that's the problem, because the ThreatMeter of a character (Tony), might need a reference of another character which hasn't been loaded yet (Mike), because Mike was saved after Tony. I hope it makes sense.

    For now I've split the saving and loading in two "phases":
    Phase 1 - Save/load the data that does not requires a reference.
    Phase 2 - Save/load the data that requires a reference.

    So I save Tony and Mike, and only after that I save Tony's data that requires Mike's ref. This ways when the games loads it creates Tony and Mark and then it loads the remaining data of Tony that requires Mark's ref.
     
  13. Bisoncraft

    Bisoncraft

    Joined:
    Feb 5, 2016
    Posts:
    37
    OK, it makes sense. I think the method you currently use has a big flaw, which is that you won't be able to handle characters with references to each other, for instance if Mike has a reference to Tony, and Tony has a reference to Mike.

    So I think the method I've used should work for you as well.

    I would suggest:
    - remove these lines of code from ReadXML:
    Code (CSharp):
    1. ITarget enemy = Zone.Current.CharacterManager.GetCharacterWithID(id);
    2.                     if (enemy != null)
    3.                     {
    4.                         AddEnemy(enemy, threat);
    5.                     }
    and add them to a new method, called for instance RefreshLink.

    Then you need to add a second method from WorldController with something like that:
    Code (CSharp):
    1. foreach (Character c in AllCharacters)
    2. {
    3. c.RefreshLinks();
    4. }
    (obviously you need to adapt to your code, but I imagine you have a list of all your characters that you can call).