Сложная структура данных дерева

Первый привет:) Это будет долгая поездка, так голая со мной, если хотите, я обещаю, что это будет весело:)

Я работаю над системой элементов для игры, которую мы делаем аналогично старым классическим зверским званиям Resident. В настоящее время я реализую элементы, объединяющие, где вы комбинируете разные элементы друг с другом, чтобы получить что-то новое. Осложнения возникают из-за того, что существуют предметы, которые имеют более одного уровня трансформации, и более одного помощника для каждого уровня. Позвольте мне уточнить, допустим, что у нас есть зеленая, красная и синяя травы. Вы не можете комбинировать красный + синий, однако вы можете комбинировать G + B, чтобы получить GreenBlueHerb, или G + R, чтобы получить GreenRedHerb, теперь, если вы объедините любой из этих результатов с синей травой, вы получите GreyHerb. Как вы видите из этого примера, есть 2 уровня трансформации для зеленой травы, так как она достигает первого уровня, есть два возможных помощника (красный | синий), с этого момента идет на второй уровень, там только один помощник (синий).

Итак, я придумал интересное дерево, охватывающее все возможности, которые идут вниз nLevels, а не только 2, тем больше уровни сложнее дерева, посмотрите на этот пример из трех уровней, форма треугольника, которую вы видите в середине, представляет элемент, а другие цветные фигуры вокруг него представляют собой возможные помощники для достижения следующего уровня:

enter image description here

Там много разных комбинаций, я мог бы сначала объединить свой предмет с синим, затем красный, затем зеленый или зеленый, затем красный, затем синий и т.д. Чтобы достичь моего финального уровня. Я придумал это дерево, представляющее все возможные комбинации:

enter image description here

(Числа справа - это #levels, слева - #nodes на каждом уровне). Но, как вы можете видеть, это избыточно. Если вы посмотрите на конечные узлы, все они должны быть едины, потому что все они ведут к одному и тому же окончательному результату, который представляет собой G + R + B. Для этой ситуации существует всего 7 возможных состояний, вот правильное дерево:

enter image description here

Это имеет большой смысл, обратите внимание на огромную разницу в количестве узлов.

Теперь мой вопрос: какова правильная структура данных для этого? - Я почти уверен, что для этого нет встроенного, поэтому мне нужно будет сделать свой собственный, который я действительно сделал, и мне удалось заставить его работать, но с проблемой. (Одна вещь, о которой стоит упомянуть, это то, что я получаю информацию о узлах из файла XML, по информации, я имею в виду itemRequired, чтобы достичь уровня node/, и что будет именем моего элемента в что node, например: для зеленой травы, чтобы достичь состояния RedGreenHerb, для нее требуется "a RedHerb, когда эта комбинация происходит, имя" GreenHerb "изменится на" RedGreenHerb ", и если вам интересно, что происходит с RedHerb, оно просто исчезает, мне больше не нужно) вот мои структуры данных:

public struct TransData
{
    public TransData(string transItemName, string itemRequired)
    {
        this.transItemName = transItemName;
        this.itemRequired = itemRequired;
    }
    public string transItemName;
    public string itemRequired;
}

public class TransNode
{
    public List<TransNode> nodes = new List<TransNode>();
    public TransData data;
    public TransNode(TransNode node): this(node.data.transItemName, node.data.itemRequired) { }
    public TransNode(string itemName, string itemRequired)
    {
       data = new TransData(itemName, itemRequired);
    }
}

public class TransLevel
{
    public List<TransNode> nodes = new List<TransNode>();
    public TransNode NextNode { get { return nodes[cnt++ % nodes.Count]; } }
    int cnt;
}

public class TransTree
{    
    public TransTree(string itemName)
    {
        this.itemName = itemName;
    }
    public string itemName;
    public TransNode[] nodes;
    public List <TransLevel> levels = new List<TransLevel>();
    // other stuff...
}

Позвольте мне объяснить: TransTree на самом деле является базой node, которая имеет название элемента при запуске ( GreenHerb для ex), дерево имеет несколько уровней (строки в черный, который вы видите на рисунках), каждый уровень имеет несколько узлов, каждый node несет с собой новые данные элемента и несколько узлов, которые указывают на это (дочерние узлы). Теперь вы можете спросить, в чем состоит необходимость размещения списка узлов в классе TransTree? - Я отвечу, что после того, как я покажу вам, как я получаю данные из своего XML файла:

public TransTree GetTransItemData(string itemName)
{
    var doc = new XmlDocument();
    var tree = new TransTree(itemName);
    doc.LoadXml(databasePath.text);

    var itemNode = doc.DocumentElement.ChildNodes[GetIndex(itemName)];
    int nLevels = itemNode.ChildNodes.Count;
    for (int i = 0; i < nLevels; i++) {
       var levelNode = itemNode.ChildNodes[i];
       tree.levels.Add(new TransLevel());
       int nPaths = levelNode.ChildNodes.Count;
       for (int j = 0; j < nPaths; j++) {
            var pathNode = levelNode.ChildNodes[j];
        string newName = pathNode.SelectSingleNode("NewName").InnerText;
        string itemRequired = pathNode.SelectSingleNode("ItemRequired").InnerText;
        tree.levels[i].nodes.Add(new TransNode(newName, itemRequired));
       }
    }
    tree.ConnectNodes(); // pretend these two
    tree.RemoveLevels(); // lines don't exist for now
    return tree;

}

Здесь образец XML, чтобы все было ясно: ItemName- > Level- > Path (это не что иное, как node) → Данные пути

<IOUTransformableItemsDatabaseManager>
  <GreenHerb>
    <Level_0>
      <Path_0>
        <NewName>RedGreenHerb</NewName>
        <ItemRequired>RedHerb</ItemRequired>
      </Path_0>
      <Path_1>
        <NewName>BlueGreenHerb</NewName>
        <ItemRequired>BlueHerb</ItemRequired>
      </Path_1>
    </Level_0>
    <Level_1>
      <Path_0>
        <NewName>GreyHerb</NewName>
        <ItemRequired>BlueHerb</ItemRequired>
      </Path_0>
    </Level_1>
  </GreenHerb>
</IOUTransformableItemsDatabaseManager>

Теперь проблема с этим заключается в том, что узлы не связаны друг с другом, так что, что это значит? Ну, если элемент занимает определенный путь до определенного уровня, то нам, разумеется, не нужно сохранять другие пути, которых это не берет, поэтому зачем хранить их в памяти? (там нет перехода назад, как только вы пойдете по пути, что вы обязаны следовать этому пути и никогда не оглядываться назад) Что бы я хотел иметь, это когда я вынимаю node, все остальные узлы под ним тоже будет падать, что имеет смысл, но в настоящее время я так делаю это примерно так:

enter image description here

Как вы можете видеть, узлы не связаны, что их удерживает уровень! это означает, что в настоящее время мне не удается вынуть node таким образом, чтобы он вынимал все его дочерние узлы. (Выполнение этого без того факта, что узлы связаны очень тяжело, и может сильно ухудшить производительность). Это приводит нас к:

tree.ConnectNodes(); 
tree.RemoveLevels();

Сначала подключаю узлы, а затем удаляю уровни? почему, потому что, если я этого не сделаю, то каждый node имеет две ссылки на него: одну из ее родительской node и другую с текущего уровня. Теперь ConnectNode на самом деле для длинного дерева, которое я показал, а не для оптимизированного с 7 состояниями:

    // this is an overloaded version I use inside a for loop in ConnectNodes()
    private void ConnectNodes(int level1, int level2)
    {
        int level1_nNodes = levels[level1].nodes.Count;
        int level2_nNodes = levels[level2].nodes.Count;

        // the result of the division gives us the number of nodes in level2,
        // that should connect to each node in level1. 12/4 = 3, means that each
        // node from level1 will connect to 3 nodes from level2;
        int nNdsToAtch = level2_nNodes / level1_nNodes;
        for (int i = 0, j = 0; i < level2_nNodes; j++)
        {
            var level1_nextNode = levels[level1].nodes[j];
            for (int cnt = 0; cnt < nNdsToAtch; cnt++, i++)
            {
                var level2_nextNode = levels[level2].nodes[i];
                level1_nextNode.nodes.Add(new TransNode(level2_nextNode));
            }
        }
   }

Это, в конечном счете, то, что я хочу иметь на своем другом дереве, но я не знаю, как это сделать. Я хочу связать узлы и сформировать второе дерево, которое я показал, что относительно проще, чем 4 уровня для ex, я даже не мог соединить узлы в краске! (когда я пробовал 4 уровня)

Если вы посмотрите на него, вы найдете некоторое сходство с двоичными числами, здесь мое дерево в двоичной форме:

001
010
100
011
101
110
111

Каждый '1' представляет собой фактический элемент, '0' означает пустой. Из моего дерева, '001' = Синий, '010' = Зеленый, '100' = Красный, '011' означает Зеленый + Синий,... '111' = Серый (конечный уровень)

Итак, теперь я все объяснил, во-первых: мой подход правильный? Если нет, то что есть? Если да, то какова структура данных, которую я мог бы использовать/сделать для достижения этой цели? и если структуры данных, которые я придумал, находятся на их месте, как я могу хранить данные из файла XML в своей структуре данных так, чтобы они соединяли узлы вместе, поэтому всякий раз, когда я вынимаю node, он извлекает его узлы с ним?

Большое спасибо за вашу помощь и терпение:)

EDIT: Интересно отметить, что вся эта система предназначена для предметов, которые встречаются только один раз на протяжении всей игры (получает один раз). Вот почему, когда я беру путь, я удаляю его из memeory, а также всякий раз, когда я беру элемент, я удаляю его запись из базы данных, потому что я больше не буду встречаться с ней.

РЕДАКТИРОВАТЬ: Обратите внимание, что я не только представляю свои объекты только с помощью строк, у них много других свойств. Но в этой ситуации меня волнуют только их имена, поэтому я имею дело со строками.

Ответ 1

ОК, наконец, после того, как я возился и смотрел на всю систему в течение нескольких часов, вчера я придумал идею, я ее реализовал, и все получилось очень хорошо:) Я просто протестировал ее на трех уровнях трансформаций и он работал как магия. (на pic показан только один комбо, но я заверяю вас, что все комбо работают)

enter image description here

Позвольте мне поделиться своим решением. Было несколько настроек, которые я должен был сделать в своей предыдущей системе, сначала скажу, что это за хитрости, и почему я сделал каждый из них.

  • Я изменил данные, хранящиеся на каждом node. В моем предыдущем подходе я если бы каждый node зависел от предыдущего, теперь требования к узлам происходит непосредственно из корня.

Вот как выглядит структура:

public struct TransData
{
    public TransData(string itemName, List <string> itemsRequired)
    {
        this.itemName = itemName;
        this.itemsRequired = itemsRequired;
    }
    public string itemName;
    public List <string> itemsRequired;
}

Node конструктор:

public TransNode(string itemName, List <string> itemsRequired)
{
    data = new TransData(itemName, itemsRequired);
}

Пример того, как теперь обрабатываются требования:

Necklace L.1_A: Requires BlueGem
Necklace L.1_B: Requires GreenGem
Necklace L.1_C: Requires RedGem
Necklace L.2_A: Requires BlueGem AND GreenGem
Necklace L.2_B: Requires GreenGem AND RedGem
Necklace L.2_C: Requires RedGem AND BlueGem
Necklace L.3_A: Requires RedGem AND BlueGem AND GreenGem

Теперь XML:

<IOUTransformableItemsDatabaseManager>
    <Necklace>
        <Level_0>
            <Path_0>
                <NewName>RedNecklace</NewName>
                <ItemsRequired>
                    <Item_0>Red</Item_0>
                </ItemsRequired>
            </Path_0>
            <Path_1>
                        .......
            </Level_0>
        <Level_1>
            <Path_0>
                <NewName>RedGreenNecklace</NewName>
                <ItemsRequired>
                    <Item_0>Red</Item_0>
                    <Item_1>Green</Item_1>
                </ItemsRequired>
            </Path_0>
            <Path_1>
                  .....
        </Level_1>
        <Level_2>
            <Path_0>
                <NewName>RedGreenBlueNecklace</NewName>
                <ItemsRequired>
                    <Item_0>Red</Item_0>
                    <Item_1>Green</Item_1>
                    <Item_2>Blue</Item_2>
                </ItemsRequired>
            </Path_0>
        </Level_2>
    </Necklace>
</IOUTransformableItemsDatabaseManager>
  • Я добавил root node к моему дереву и удалил список nodes это имеет смысл. Этот предыдущий список nodes теперь равен  до root.nodes

Вот как выглядит дерево:

public class TransTree
{
    //public string itemName;
    //public List <TransNode> nodes;
    public List <TransLevel> levels = new List<TransLevel>();
    public TransNode root { private set; get; }
    public TransTree(string itemName)
    {
    //  this.itemName = itemName;
        root = new TransNode(itemName, null);
    }
}
  • Теперь, почему я сделал первое изменение? (требования) Поскольку это позволил мне наладить связь между узлами, которые, в свою очередь, позволили мне, наконец, подключить их так, как я точно (как они связаны в дереве с 7 состояниями выше в моем Q) Как? - В моем примере ожерелья, какая связь между L.1_A и L.2_A? Ответ: L.1_A имеет один из требований L.2_A, который является BlueGem, они оба имеют это общее, что приводит нас к заключение:

Всякий раз, когда два узла, разделенных одним уровнем между ними, имеют что-то в общий, node на вышеуказанном уровне должен указывать на один выше

Итак, все, что я делаю, это просто перебрать все узлы на одном уровне и сравнить каждый из этих узлов уровня с каждым node на следующем уровне, если node на первом уровне имеет одно требование: node на следующем уровне есть, есть соединение:

public void ConnectNodes()
{
   for (int i = 0; i < levels.Count - 1; i++)
       ConnectNodes(i, i + 1);
   ConnectRoot();
}
private void ConnectNodes(int level1, int level2)
{
    int level1_nNodes = levels[level1].nodes.Count;
    int level2_nNodes = levels[level2].nodes.Count;
    for (int i = 0; i < level1_nNodes; i++)
    {
        var node1 = levels[level1].nodes[i];
        for (int j = 0; j < level2_nNodes; j++)
        {
            var node2 = levels[level2].nodes[j];
            foreach (var itemReq in node1.data.itemsRequired)
            {
                if (node2.data.itemsRequired.Contains(itemReq))
                {
                    node1.nodes.Add(node2);
                    break;
                }
            }
        }
     }
}

Обратите внимание на очень важную строку:

ConnectRoot();

public void ConnectRoot()
{
    foreach (var node in levels[0].nodes)
       root.nodes.Add(node);
}

Это ясно? - Я просто подключаю корень node к узлам первого уровня, а затем подключаю узлы уровня 1 к 2, от 2 до 3 и т.д. Конечно, я должен сделать это, прежде чем очищать уровни. Это не изменилось:

// fetching the data from the xml file
    tree.ConnectNodes();
    tree.RemoveLevels();

Почему я сделал второе изменение? (добавлен корень node к дереву).  Ну, вы можете ясно видеть преимущества наличия корня node, он  имеет больше смысла. Но главная причина, по которой я реализовал эту корневую идею,  это легко уничтожить узлы, которые я не возьму. Я упомянул в своем  Q, что всякий раз, когда я беру путь /node, узлы того же уровня  должен уйти, потому что это, я пошел по дороге, не собираюсь  назад. С корнем под рукой, теперь я мог бы легко сказать:

tree.SetRoot(nodeTakenToTransform);

Не нужно переходить через те узлы, которые я не брал, и обнулить их, просто изменив корень дерева на node, который я взял, что имеет дополнительное преимущество, чтобы освободить меня от бремени обработки моего дерева как некоторый sorta связанный список, (чтобы получить доступ к node по дереву, мне нужно пройти через корень, а что между корнем и node я хочу получить и, наконец, доехать до места назначения) - после каждого преобразования root уходит на один уровень, теперь мне нужно получить доступ к корневым узлам.

Осталась еще одна проблема. Это метод внутри моего пользовательского словаря для обработки элементов, когда элемент преобразуется, он "уведомляет" словарь о необходимости принятия необходимых действий (обновляет его ключи, значения и т.д.)

public void Notify_ItemTransformed(TransTree itemTree, TransNode nodeTaken)
{
   var key = new Tuple<string, string>(itemTree.root.data.itemName, nodeTaken.data.itemsRequired[0]);
   itemTree.SetRoot(nodeTaken);
   itemTree.UpdateNodesRequirements(itemTree.root); // take note here
   RemoveKey(key);
   RegisterItem(itemTree);
}

Что делает itemTree.UpdateNodesRequirements(itemTree.root)? Мое первое изменение ввело проблему. Например: Когда я достиг L.1_A, у меня уже есть BlueGem? иначе я бы не достиг такого уровня. Но проблема в том, что все узлы, после этого уровня, требующие BlueGem, все еще требуют его, хотя у меня есть это! Они не должны просить об этом, т.е. BlueGreenNecklace должен теперь требовать GreenGem. Вот почему теперь мне нужно обновить эти требования узлов, путем рекурсивного перехода с моего нового корня:

tree.SetRoot(nodeTaken);
tree.UpdateNodesRequirements(tree.root);

Здесь метод:

public void UpdateNodesRequirements(TransNode node)
{
    foreach (var n in node.nodes)
    {
        if (n.data.itemsRequired.Contains(root.data.itemsRequired[0]))
            n.data.itemsRequired.Remove(root.data.itemsRequired[0]);
        if (n.nodes != null && n.nodes.Count > 0)
            UpdateNodesRequirements(n);
    }
}

Все, что я говорю, "дорогой следующий уровень node, если вам требуется что-то, что у меня уже есть, пожалуйста, удалите это требование" - И это:) Надеюсь, вам понравилась моя статья XD - я поставлю следующую видео-демонстрацию в вопрос обновление когда это.

С точки зрения производительности? Хм, там несколько мест, где я мог бы сделать некоторые оптимизации, но на данный момент тестирование с 3 уровнями было действительно хорошим, без ухудшения вообще. Я сомневаюсь, что мой игровой дизайнер придумает что-то, что требует более 4 уровней, но даже если он это сделает, мой код должен быть достаточно быстрым, а если нет, я мог бы использовать yield return null в соответствующих местах, чтобы разделить работу на разных кадрах (Я использую Unity3D)

Что мне действительно нравится в этом решении, так это то, что оно работает для всех видов деревьев преобразования, даже если оно не было симметричным, для nPaths и nLevels:)

Спасибо всем, кто пытался помочь, и потратил время, чтобы прочитать это. - Vexe

Ответ 2

Что мне не нравится в этом решении:

  • Простые решения - лучшие решения.
  • Сложно поддерживать, поскольку ваш xml основан на графике.
  • Не используйте OOP
  • Источник ошибок
  • Вероятное использование Reflection для небольших проблем (я говорю, потому что, если у вас будет такая игра, вы столкнетесь с гораздо более трудными проблемами;)). Это подразумевает излишнюю сложность.

Что мне нравится в этом решении:

  • Вы только прекрасно поняли проблему. У каждого элемента есть список трансконкций с некоторыми другими объектами. Теперь проблема заключается в том, как представлять (а не хранить)

Что бы я сделал (juste IMHO, ваше решение тоже хорошо): используйте OOP в node - только с точки зрения. Таким образом, ваше дерево станет машиной состояния (как вы говорили о пути;)), если вы хотите прикрепить ее к структуре данных.

public class InventoryObject
{
    protected Dictionnary<Type, InventoryObject> _combinations = new Dictionnary<Type, InventoryObject>();

    public InventoryObject() {}       

    public InventoryObject Combine(InventoryObject o)
    {
       foreach (var c in _combinations)
          if (typeof(o) == c.Key)
            return c.Value

       throw new Exception("These objects aren't combinable");
    }
}

public class BlueHerb : InventoryObject
{
    public Herb()
    {
       _combinations.Add(RedHerb, new BlueRedHerb());
       _combinations.Add(GreenHerb, new BlueGreenHerb());
    }
}

public class BlueRedHerb: InventoryObject
{
    public BlueRedHerb()
    {
       _combinations.Add(GreenHerb, new GreyHerb());
    }
}

Затем просто вызовите BlueHerb.Combine(myRedHerb);, чтобы получить результат. Вы также можете сделать BlueHerb.Combine(myStone); и легко отлаживать.

Я стараюсь, чтобы мой пример был как можно более простым. Для подсветки кода (класс Herb, класс CombinedHerb, использование запросов LINQ и т.д.) Может быть сделано много изменений.