Сократить код обращения к одинаковым свойствам классов, когда они реализованы разными базовыми классами/интерфейсами?

201
27 декабря 2021, 22:00

Пишу простой tweener на Unity. Движок предоставляет классы (компоненты) для рендера, и у тех, что я использую есть свойство color, но эти классы не наследуются от чего то, что имеет это свойство и не реализуют единого интерфейса, что обязывал бы реализовывать свойство цвета. В итоге мне приходиться писать повторяющийся код:

            foreach (var mesh in texts)
            {
                Color delta = (endColor - mesh.color) / frames;
                for (int i = 0; i < frames; i++)
                {
                    mesh.color += delta;
                    yield return new WaitForEndOfFrame();
                }
            }
            foreach (var mesh in images)
            {
                Color delta = (endColor - mesh.color) / frames;
                for (int i = 0; i < frames; i++)
                {
                    mesh.color += delta;
                    yield return new WaitForEndOfFrame();
                }
            }
            foreach (var mesh in sprites)
            {
                Color delta = (endColor - mesh.color) / frames;
                for (int i = 0; i < frames; i++)
                {
                    mesh.color += delta;
                    yield return new WaitForEndOfFrame();
                }
            }

Казалось бы, очевидным решением было бы использовать dynamic тип, но как известно Microsoft немного накосячили и у нас в .Net Framework 4.0 кривой RuntimeBinder и использовать dynamic тип я не могу (так-то могу, но это затратно по ресурсам и времени написания и вообще это всё были бы костыли). Так же не хочу использовать явное приведение внутри цикла, так как это более нечитаемый код и менее эффективно по ресурсам, хоть и ненамного.

Так как же всё таки сократить это?

Полный код функции:

public static GameObject Paint(this GameObject obj, Color endColor, int frames, float wait = float.NaN)
    {
        IEnumerator AwaitPaint()
        {
            var texts = new List<TextMesh>();
            var images = new List<Image>();
            var sprites = new List<SpriteRenderer>();
            void AddComponents()
            {
                var childrens = new List<GameObject>();
                void AddChildrens(GameObject _obj)
                {
                    var newChildrens = new List<GameObject>();
                    foreach (Transform tr in _obj.transform)
                    {
                        childrens.Add(tr.gameObject); 
                        newChildrens.Add(tr.gameObject);
                    }
                    foreach (GameObject __obj in newChildrens.ToArray())
                    {
                        AddChildrens(__obj);
                    }
                }
                AddChildrens(obj);
                foreach (var text in obj.GetComponents<TextMesh>())
                {
                    texts.Add(text);
                }
                foreach (var image in obj.GetComponents<Image>())
                {
                    images.Add(image);
                }
                foreach (var sprite in obj.GetComponents<SpriteRenderer>())
                {
                    sprites.Add(sprite);
                }
            }
            AddComponents();
            if (!float.IsNaN(wait))
            {
                yield return new WaitForSeconds(wait);
            }
            foreach (var mesh in texts)
            {
                Color delta = (endColor - (Color)mesh.color) / frames;
                for (int i = 0; i < frames; i++)
                {
                    mesh.color += delta;
                    yield return new WaitForEndOfFrame();
                }
            }
            foreach (var mesh in images)
            {
                Color delta = (endColor - (Color)mesh.color) / frames;
                for (int i = 0; i < frames; i++)
                {
                    mesh.color += delta;
                    yield return new WaitForEndOfFrame();
                }
            }
            foreach (var mesh in sprites)
            {
                Color delta = (endColor - (Color)mesh.color) / frames;
                for (int i = 0; i < frames; i++)
                {
                    mesh.color += delta;
                    yield return new WaitForEndOfFrame();
                }
            }
            called.Remove(called.Same(obj));
        }
        if (called.FindIndex((x) => x.obj == obj) >= 0)
        {
            CoroutineContainer.Stop(called.Same(obj).coroutine);
            called.Remove(called.Same(obj));
        }
        called.Add((obj, CoroutineContainer.Start(AwaitPaint())));
        return obj;
    }

Вызов:

gameObject.Paint(new Color(1, 1, 1, 1), 30);
Answer 1

Есть такая замечательная вещь - паттерн проектирования "Адаптер", который позволяет совместить несовместимое.

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

Для начала нужно определиться, что нам вообще нужно получить от всех этих объектов:

  1. Возможность получить текущий цвет
  2. Возможность изменить текущий цвет

Для чего реализуем интерфейс:

public interface IColorChanger {
    void ChangeColor(Color delta);
    Color GetColor();
}

Реализуем этот интерфейс для конкретного типа TextMesh (все остальное в самом конце):

public class TextMeshColorAdapter : IColorChanger {
    private TextMesh _target;
    public TextMeshColorAdapter(TextMesh target) {
        _target = target;
    }
    public void ChangeColor(Color delta) {
        _target.color += delta;
    }
    public Color GetColor() {
        return _target.color;
    }
}

Не совсем относится к вопросу, но решил и это исправить:

Не стоит писать свои велосипеды по рекурсивному поиску всех потомков объекта, метод GetComponentsInChildren сделает все сам.

Адаптеры разных компонентов можно объединить в один список и вместо кучи циклов сделать парочку LINQ запросов:

var targets = new List<IColorChanger>();
targets.AddRange(
    obj.GetComponentsInChildren<TextMesh>().Select(txt => new TextMeshColorAdapter(txt))
);
targets.AddRange(
    obj.GetComponentsInChildren<Image>().Select(img => new ImageColorAdapter(img))
);
targets.AddRange(
    obj.GetComponentsInChildren<SpriteRenderer>().Select(sprite => new SpriteRendererColorAdapter(sprite))
);

Благодаря общему интерфейсу также можно один раз пройтись по общему массиву и поменять цвет каждого элемента:

foreach (var target in targets) {
    var deltaColor = (endColor - target.GetColor()) / frames;
    for (int i = 0; i < frames; i++)
    {
        target.ChangeColor(deltaColor);
        yield return new WaitForEndOfFrame();
    }
}

Полный код

Адаптеры

public interface IColorChanger {
    void ChangeColor(Color delta);
    Color GetColor();
}
public class TextMeshColorAdapter : IColorChanger {
    private TextMesh _target;
    public TextMeshColorAdapter(TextMesh target) {
        _target = target;
    }
    public void ChangeColor(Color delta) {
        _target.color += delta;
    }
    public Color GetColor() {
        return _target.color;
    }
}
public class ImageColorAdapter : IColorChanger {
    private Image _target;
    public ImageColorAdapter(Image target) {
        _target = target;
    }
    public void ChangeColor(Color delta) {
        _target.color += delta;
    }
    public Color GetColor() {
        return _target.color;
    }
}
public class SpriteRendererColorAdapter : IColorChanger  {
    private SpriteRenderer _target;
    public SpriteRendererColorAdapter(SpriteRenderer target) {
        _target = target;
    }
    public void ChangeColor(Color delta) {
        _target.color += delta;
    }
    public Color GetColor() {
        return _target.color;
    }
}

Метод

public static GameObject Paint(this GameObject obj, Color endColor, int frames, float wait = float.NaN)
{
    IEnumerator AwaitPaint()
    {
        var targets = new List<IColorChanger>();
        targets.AddRange(obj.GetComponentsInChildren<TextMesh>().Select(txt => new TextMeshColorAdapter(txt)));
        targets.AddRange(obj.GetComponentsInChildren<Image>().Select(img => new ImageColorAdapter(img)));
        targets.AddRange(obj.GetComponentsInChildren<SpriteRenderer>().Select(sprite => new SpriteRendererColorAdapter(sprite)));
        if (!float.IsNaN(wait))
        {
            yield return new WaitForSeconds(wait);
        }
        foreach (var target in targets) {
            var deltaColor = (endColor - target.GetColor()) / frames;
            for (int i = 0; i < frames; i++)
            {
                target.ChangeColor(deltaColor);
                yield return new WaitForEndOfFrame();
            }
        }
        called.Remove(called.Same(obj));
    }
    if (called.FindIndex((x) => x.obj == obj) >= 0)
    {
        CoroutineContainer.Stop(called.Same(obj).coroutine);
        called.Remove(called.Same(obj));
    }
    called.Add((obj, CoroutineContainer.Start(AwaitPaint())));
    return obj;
}
Answer 2

Можно написать универсальный обработчик типа

static public IEnumerator ColorForList<T> (this List<T> list, Color endColor, float frames) {
    foreach (var element in list) {
        Color delta = (endColor-element.GetColor())/frames;
        for (int i = 0; i < frames; i++) {
            element.SetColor(delta);
            yield return new WaitForEndOfFrame();
        }
    }
}
static Color GetColor<T> (this T someType) { ... }
static Color SetColor<T> (this T someType, Color color) { ... }

Чтобы потом просто вызвать

StartCoroutine(texts.ColorForList(endColor, frames));
StartCoroutine(images.ColorForList(endColor, frames));
StartCoroutine(sprites.ColorForList(endColor, frames));
Answer 3

ну, в принципе, можна попробовать так
способ крайне экзотический
да и кода кажись больше вышло, аахахахахахахахахахха
но всё же )
но лучше оставь своё, это тот случай когда повтор кода уместен

using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;

class S
{
    delegate void VoidObjectColor(object o, Color c);
    delegate Color ColorObject(object o);
    delegate void VoidColorInt(Color c, int n);
    VoidColorInt dgFinished;
    VoidObjectColor dgColorChanged;
    ColorObject dgGetColor;
    List<TextMesh> texts;
    List<Image> images;
    List<SpriteRenderer> sprites;
    /// <summary>
    /// к этому обьекту присоединим сопрограммы
    /// </summary>
    MonoBehaviour behaviour;

    public static GameObject Paint(this GameObject obj, Color endColor, 
        int frames, float wait = float.NaN)
    {
        //
        // ну вот , вызываешь, все анимации затем выполнятся автоматически
        //
        TextAnimations(endColor, frames);
    }


    IEnumerator ChangeColorS(IEnumerator ie, Color targetColor, int fframes)
    {
        //
        // желательно
        // но не обязательно
        //
        if (ie == null) yield break;
        ie.Reset();
        //////
        Color color, deltaColor;
        while(ie.MoveNext())
        {
            color = dgGetColor(ie.Current);
            deltaColor = (targetColor - color) / fframes;
            for(int i = 0; i < fframes; i++)
            {
                color += deltaColor;
                dgColorChanged(ie.Current, color);
                yield return null;
            }
        }
        if(dgFinished != null) dgFinished(targetColor, fframes);
    }

    void TextAnimations(Color targetColor, int frames)
    {
        dgGetColor = (oobject) => ((TextMesh)oobject).color;
        dgColorChanged = (oobject, color) => ((TextMesh)oobject).color = color;
        dgFinished = ImageAnimations;
        behaviour.StartCoroutine(
            ChangeColorS(texts.GetEnumerator(), targetColor, frames));
    }
    void ImageAnimations(Color targetColor, int frames)
    {
        dgGetColor = (oobject) => ((Image)oobject).color;
        dgColorChanged = (oobject, color) => ((Image)oobject).color = color;
        dgFinished = SpriteAnimations;
        behaviour.StartCoroutine(
            ChangeColorS(images.GetEnumerator(), targetColor, frames));
    }
    void SpriteAnimations(Color targetColor, int frames)
    {
        dgGetColor = (oobject) => ((SpriteRenderer)oobject).color;
        dgColorChanged = (oobject, color) => ((SpriteRenderer)oobject).color = color;
        dgFinished = null;
        behaviour.StartCoroutine(
            ChangeColorS(sprites.GetEnumerator(), targetColor, frames));
    }
}
READ ALSO
как мне перехватить этот вид исключения

как мне перехватить этот вид исключения

Как в laravel сделать если не авторизован пользователь и вызвать Auth::user()->id; то возвращается ошибка Trying to get property of non-object как ее заменить на свое...

129
Условие на значение атрибута WooCommerce

Условие на значение атрибута WooCommerce

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

188
Условие. Вывод без дублей из под foreach?

Условие. Вывод без дублей из под foreach?

И выдает собака: ok ok ok ok ok ok ok ok ok ok ok ok ok okNONONONONONONONONONONO

168
Wordpress вывод произвольного поля как meta-description

Wordpress вывод произвольного поля как meta-description

Использую плагин ACF для настройки вывода пользовательского контента через произвольные поляПомогите, пожалуйста, с решением задачи — вывод...

165