Реализация перебора объектов в цикле при помощи IEnumerable и IEnumerator.

Unity Logo
Чет снова я подзабил на сайт – стыдно. Надо исправляться…

Читал я тут одну книжку, и узнал интересный прием перебора в цикле однотипных объектов, не помещая их заранее в массив/список/коллекцию/etc.. Проще будет показать на примере. Для наглядности, пример будет в Unity.

Вводная.

Допустим в нашей игре есть тип противников, которые спаунятся динамически и рандомно. По этой причине, мы не можем их запихнуть в статический массив, при инициализации сцены. А из-за слишком частого изменения их количества, нам нецелесообразно иметь динамический массив. Ну или по каким-то иным причинам.

Есть в Unity функция FindObjectsOfType, которая позволяет получить массив объектов заданного типа, но она работает медленно, поэтому пригодна только для каких-то разовых инициализаций. Например, для инициализации массива при загрузке сцены.

Итак, задача заключается в следующем: нам нужно реализовать возможность перебора объектов Alien в цикле, без использования массивов, списков и т.п.. Прошаренные кодеры, наверное, уже догадались, что поможет нам в этом реализация интерфейсов IEnumerable и IEnumerator.

Класс Alien

Первым делом напишем класс Alien, точнее ту его часть, которая позволит нам перебирать его в цикле.

Унаследуем наш класс от интерфейса IEnumerable:

public class Alien : MonoBehaviour, IEnumerable

и реализуем единственный метод интерфейса:

//Возвращает итератор
public IEnumerator GetEnumerator()
{
    return new AlienEnumerator();
}

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

Создаем 4 свойства для связывания объекта Alien с другими экземплярами класса, они нужны для навигации:

//Статические ссылки на первый и последний созданный объекты
public static Alien FirstCreated { get; private set; }
public static Alien LastCreated { get; private set; }
 
//Ссылки на следующий и предыдущий объекты относительно данного экземпляра
public Alien NextAlien { get; private set; }
public Alien PrevAlien { get; private set; }

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

Ссылки же на следующий и предыдущий объекты уникальные для каждого конкретного экземпляра.

Теперь нам нужно написать логику связывания этих ссылок с соответствующими объектами. Делать это нужно при создании объекта в конструкторе, но так, как класс наследует MonoBehaviour, мы не можем воспользоваться конструктором, поэтому воспользуемся методом Awake().

//Аналог конструктора для скриптов MonoBehaviour
void Awake()
{
    //Ставим "себя" первым созданным объектом, если до этого объекты не создавались  
    //или были все удалены
    if (FirstCreated == null)
        FirstCreated = this;
 
    if (LastCreated != null)
    {
        //Ставим "себя" следующим объектом для последнего созданного экземпляра
        LastCreated.NextAlien = this;
        //Создаем ссылку на предыдущий объект (последний созданный)
        this.PrevAlien = LastCreated;
    }
 
    //Ставим "себя" последним созданным объектом
    LastCreated = this;
}

В общем-то в комментариях все разжевано.

Так как наши объекты могут быть уничтожены, то нужно это обработать, иначе вся цепочка ссылок развалится, для этого воспользуемся событием MonoBehaviour – OnDestroy():

//Обработчик события уничтожения объекта
void OnDestroy()
{
    //Заменяем ссылку на "себя" у предыдущего объекта,
    //на ссылку следующего относительно "нас" объекта
    if (PrevAlien != null)
        PrevAlien.NextAlien = NextAlien;
 
    //Заменяем ссылку на "себя" у следующего объекта,
    //на ссылку предыдущего относительно "нас" объекта
    if (NextAlien != null)
        NextAlien.PrevAlien = PrevAlien;
}

Вот и все, класс готов к перебору.

Теперь нужно реализовать класс, благодаря которому и будет осуществляться перебор.

Класс AlienEnumerator

Данный класс состоит всего из нескольких строк:

public class AlienEnumerator : IEnumerator
{
    //Ссылка на текущий объект
    private Alien _currentObj;
 
    //Свойство для доступа к текущему объекту
    public object Current
    {
        get { return _currentObj; }
    }
 
    //Метод перехода по объектам
    //Возвращает true если удалось перейти на следующий объект
    public bool MoveNext()
    {
        _currentObj = (_currentObj == null) ? Alien.FirstCreated : _currentObj.NextAlien;
 
        return _currentObj != null;
    }
 
    //Метод обнуления итератора
    public void Reset()
    {
        _currentObj = null;
    }
}

Вся «начинка» класса, это реализация интерфейса IEnumerator. Интерес наверное представляет только метод MoveNext(), который берет первый объект если текущий null, или следующий объект, на который ссылается текущий (_currentObj).

Вот собственно и всё. Теперь надо проверить, как это работает.

Тестирование работы.

Для тестирования работы, я написал скрипт GameManager и повесил его на одноименный объект-пустышку.

public class GameManager : MonoBehaviour
{
    // Use this for initialization
    void Start ()
    {
        for (int i = 0; i < 10; i++)
        {
            new GameObject("Alien " + i).AddComponent<Alien>();
        }
    }
 
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Return))
            TestingEnumeration();
 
        if (Input.GetKeyDown(KeyCode.Delete))
        {
            Debug.Log("//-------Test Destroy--------//");
            Destroy(GameObject.Find("Alien 5"));
        }
    }
 
    private void TestingEnumeration()
    {
        Alien aliens = Alien.FirstCreated;
 
        if(aliens == null) return;
 
        foreach (Alien alien in aliens)
            Debug.Log(alien.name);
    }
}

В методе Start() создается 10 GameObject’ов с компонентом Alien. В методе Update() мы обрабатываем нажатие клавиш: если нажата клавиша Enter, вызывается метод TestingEnumeration(), в котором и происходит перебор объектов, с выводом имени объектов в консоль. Если нажата клавиша Del: происходит уничтожение объекта с заданным именем. Теперь если после нажатия Del нажать Enter, то в консоль выведется список из 9-ти имен (без “Alien 5”). Т.е. цикл foreach отрабатывает, как и положено.

Кроме того, перебрать объекты можно еще и так:

private void TestingEnumeration()
{
    if(Alien.FirstCreated == null) return;
 
    IEnumerator AlienEnumerator = Alien.FirstCreated.GetEnumerator();
    while (AlienEnumerator.MoveNext())
    {
        Alien alien = AlienEnumerator.Current as Alien;
        Debug.Log(alien.name);
    }
}

Но выглядит не так элегантно, как foreach.

На этом всё, спасибо за внимание.

Добавить комментарий