Программная архитектура последовательности логических вентилей
Обучающее приложение по логическим операциям: игрок может собирать цепи (схемы) из логических вентилей. Я хочу, чтобы, собрав какую-то цепь, он мог объединить ее в один.
Например, собираем схему из двух вентилей (AND и NOT) и объединяем в вентиль NAND:
При этом каждый вентиль может иметь один или несколько входов (вводов) и один или несколько выходов (выводов).
Я не могу придумать, как мне прописать такой класс вентиля. Вот мои идеи:
Создать делегат: 'delegate List GateFunction(List)'
Класс вентиля содержит поле 'GateFunction func', которое определяет функцию, по которой вводы (inputs) преобразуются в выводы (outputs).
Класс вентиля содержит массив 'List _inputs', который содержит ссылки на выводы предыдущих вентилей, которые подсоединяются проводами к вводам данного вентиля.
Очень важно, что один вентиль может иметь НЕСКОЛЬКО выводов, и разные выводы одного вентиля могут потом идти к разным вентилям.
Мне нужна такая архитектура, чтобы каждый экземпляр класса хранил в себе КОНКРЕТНУЮ функцию func, оперирующую базовыми функциями.
ПРИ ЭТОМ ГЛАВНОЕ, что в рантайме пользователь будет строить и сохранять свои, новые, кастомные вентили, состоящие из ранее созданных (или базовых: and/or/not).
Буду благодарен как общим советам так и коду. Возможно, я упускаю какой-то удобный очевидный паттерн...
Идея и скриншоты взяты из видео Sebastian Lague (https://youtu.be/QZwneRb-zqA). В нем отлично показано, что мне нужно с 4 по 5 минуту.
UPD: Пример-пояснение. Игрок на поле может собрать из имеющихся блоков схемы слева, нажать на кнопочку "Create" и получить соответствующие блоки справа. При этом, как видно, все функции слева открыты, то есть могут быть подсоединены к первоначальным контактам (на 1-м скриншоте слева на экране). Эти контакты кнопкой мыши включаются и выключаются. То есть эти контакты НЕ входят в сам вентиль. Таким образом, нужна возможность указать готовому, собранному вентилю, к каким контактам присоединяются его инпуты.

UPD2: А по кнопке "Create" вентиль должен сохраняться в менюшку, чтобы потом его оттуда можно было взять и использовать:
Ответы (3 шт):
пишу псевдокодом т.к. шарп уже забыл)
class Valve {
private var type: ValveType
var maxInputs: Int { type.MaxInputs() }
var maxOutputs: Int { type.MaxOutputs() }
var inputs: List<Point> = new List<Point>
var outputs: List<Point> = new List<Point>
func actionFunc() -> Void{
switch type {
case And:
andFunc();
break;
case Or:
orFunc()
break;
default:
defaultFunc()
}
}
}
enum ValveType {
case And
case Or
case Xor
case Not
case Nand
}
extension ValveType {
func MaxInputs() -> Int {
switch self {
case Nand:
return 2;
break;
default:
return 2;
break;
}
}
func MaxOutputs() -> Int {
return 1
}
}
Ну или заделай себе фабрику которая будет выдавать вентили с необходимыми настройками
public abstract class BoolChain
{
public abstract bool GetOutValue ();
}
public class TrueChain : BoolChain
{
public override bool GetOutValue () => true;
public override string ToString () => "True";
}
public class NotChain : BoolChain
{
public BoolChain In;
public NotChain (BoolChain inChain = null)
{
In = inChain;
}
public override bool GetOutValue () => In.GetOutValue() == false;
public override string ToString () => $"Not({In})";
}
public class AndChain : BoolChain
{
public readonly BoolChain[] In;
public AndChain (BoolChain[] inChain)
{
In = inChain;
}
public AndChain (int inCapacity = 2) : this(new BoolChain[inCapacity]) { }
public override bool GetOutValue ()
{
foreach (var chain in In)
if (chain.GetOutValue() == false)
return false;
return true;
}
public override string ToString () => $"And({string.Join(", ", In.Select(i => i.ToString()))})";
}
public class OrChain : BoolChain
{
public readonly BoolChain[] In;
public OrChain (BoolChain[] inChain)
{
In = inChain;
}
public OrChain (int inCapacity = 2) : this(new BoolChain[inCapacity]) { }
public override bool GetOutValue ()
{
foreach (var chain in In)
if (chain.GetOutValue())
return true;
return false;
}
public override string ToString () => $"Or({string.Join(", ", In.Select(i => i.ToString()))})";
}
Твой BoolValve должен содержать BoolChain в котором может быть что угодно. например тот-же [False] вентиль будет содержать new NotChain(new TrueChain()) "Not(True)". Добавь прототип для копирования целой цепочки Clone(), если добавить какой ни будь флаг IsSelected, можно ограничить копирование CloneSelected() и положить ее в новый BoolValve, что бы там ни было.
Не Unity-программист, но, с учётом того что блоки должны поддерживать возможность изменения из GUI, на обычном C# это можно реализовать как-то так.
Генераторы сигнала:
// генератор сигналов (ничего не принимает на вход)
public interface ISignalGenerator
{
public bool GetOutput();
}
// генератор True-сигналов
// реализует синглтон, так как более чем в одном таком генераторе смысла нет, он может даже не быть отдельным блоком в GUI
public sealed class TrueGenerator : ISignalGenerator
{
private static TrueGenerator? s_instance;
public static TrueGenerator Instance => s_instance ??= new();
private TrueGenerator() { }
public bool GetOutput() => true;
}
// генератор False-сигналов
// реализует синглтон, так как более чем в одном таком генераторе смысла нет, он может даже не быть отдельным блоком в GUI
public sealed class FalseGenerator : ISignalGenerator
{
private static FalseGenerator? s_instance;
public static FalseGenerator Instance => s_instance ??= new();
private FalseGenerator() { }
public bool GetOutput() => false;
}
Вентиль:
// вентиль (генерирует сигнал на основе входных сигналов)
public interface ILogicGate : ISignalGenerator
{
// список генераторов для каждого из входов
public IReadOnlyList<ISignalGenerator> InputGenerators { get; }
// присоединить вход под номером inputIndex к генератору inputGenerator
public void ConnectInputWithOutput(int inputIndex, ISignalGenerator inputGenerator);
// отсоединить вход под номером inputIndex от генератора (по-сути, это тоже самое что и присоединить к False-генератору)
public void DisconnectInputGenerator(int inputIndex);
}
Вентили с множеством входом:
public abstract class MultiInputGate : ILogicGate
{
private readonly ISignalGenerator[] _inputGenerators;
public IReadOnlyList<ISignalGenerator> InputGenerators { get; }
public MultiInputGate(int inputsCount)
{
if (inputsCount <= 1)
throw new ArgumentOutOfRangeException(nameof(inputsCount));
_inputGenerators = new ISignalGenerator[inputsCount];
InputGenerators = new ReadOnlyCollection<ISignalGenerator>(_inputGenerators);
// подсоединяем все входы к False-генератору
for (int i = 0; i < inputsCount; i++)
_inputGenerators[i] = FalseGenerator.Instance;
}
public void DisconnectInputGenerator(int inputIndex)
{
if (inputIndex < 0 || inputIndex >= InputGenerators.Count)
throw new ArgumentOutOfRangeException(nameof(inputIndex));
ConnectInputWithOutput(inputIndex, FalseGenerator.Instance);
}
public virtual void ConnectInputWithOutput(int inputIndex, ISignalGenerator inputGenerator)
{
if (inputIndex < 0 || inputIndex >= InputGenerators.Count)
throw new ArgumentOutOfRangeException(nameof(inputIndex));
_inputGenerators[inputIndex] = inputGenerator ?? throw new ArgumentNullException(nameof(inputGenerator));
}
public abstract bool GetOutput();
public override string ToString()
{
var inputs = string.Join(", ", InputGenerators.Select(generator => Convert.ToInt32(generator.GetOutput())));
var output = Convert.ToInt32(GetOutput());
return $"({inputs}) ---{GetType().Name}--> {output}";
}
}
public sealed class AndGate : MultiInputGate
{
public AndGate() : base(2) { }
public override bool GetOutput() => InputGenerators[0].GetOutput() & InputGenerators[1].GetOutput();
}
Вентили с 1-м входом:
Класс SingleInputGate создан для удобного взаимодействия (например, позволяет писать gate.InputGenerator вместо gate.InputGenerators[0]).
public abstract class SingleInputGate : ILogicGate
{
private readonly ISignalGenerator[] _inputGenerators;
public ISignalGenerator InputGenerator
{
get => _inputGenerators[0];
private set => _inputGenerators[0] = value;
}
IReadOnlyList<ISignalGenerator> ILogicGate.InputGenerators => _inputGenerators;
public SingleInputGate()
{
_inputGenerators = new ISignalGenerator[] { FalseGenerator.Instance };
}
public void DisconnectInputGenerator() => ConnectInputWithOutput(FalseGenerator.Instance);
public virtual void ConnectInputWithOutput(ISignalGenerator inputGenerator) =>
InputGenerator = inputGenerator ?? throw new ArgumentNullException(nameof(inputGenerator));
public abstract bool GetOutput();
public override string ToString()
{
var input = Convert.ToInt32(InputGenerator.GetOutput());
var output = Convert.ToInt32(GetOutput());
return $"({input}) ---{GetType().Name}--> {output}";
}
void ILogicGate.DisconnectInputGenerator(int inputIndex)
{
if (inputIndex != 0)
throw new ArgumentOutOfRangeException(nameof(inputIndex));
DisconnectInputGenerator();
}
void ILogicGate.ConnectInputWithOutput(int inputIndex, ISignalGenerator inputGenerator)
{
if (inputIndex != 0)
throw new ArgumentOutOfRangeException(nameof(inputIndex));
ConnectInputWithOutput(inputGenerator);
}
}
public sealed class NotGate : SingleInputGate
{
public override bool GetOutput() => !InputGenerator.GetOutput();
}
public class DirectGate : SingleInputGate
{
public sealed override bool GetOutput() => InputGenerator.GetOutput();
}
Пользовательский вентиль:
public sealed class UserGate : ILogicGate
{
private readonly DirectGate[] _inputGates;
public DirectGate OutputGate { get; } = new();
public IReadOnlyList<DirectGate> InputGates { get; }
IReadOnlyList<ISignalGenerator> ILogicGate.InputGenerators =>
InputGates.Select(gate => gate.InputGenerator).ToArray();
public UserGate(int inputsCount)
{
if (inputsCount < 1)
throw new ArgumentOutOfRangeException(nameof(inputsCount));
_inputGates = new DirectGate[inputsCount];
InputGates = new ReadOnlyCollection<DirectGate>(_inputGates);
for (int i = 0; i < inputsCount; i++)
_inputGates[i] = new();
}
public bool GetOutput() => OutputGate.GetOutput();
public override string ToString()
{
var inputs = string.Join(", ", InputGates.Select(gate => Convert.ToInt32(gate.GetOutput())));
var output = Convert.ToInt32(GetOutput());
return $"({inputs}) ---{GetType().Name}--> {output}";
}
void ILogicGate.DisconnectInputGenerator(int inputIndex)
{
if (inputIndex < 0 || inputIndex >= InputGates.Count)
throw new ArgumentOutOfRangeException(nameof(inputIndex));
InputGates[inputIndex].DisconnectInputGenerator();
}
void ILogicGate.ConnectInputWithOutput(int inputIndex, ISignalGenerator inputGenerator)
{
if (inputIndex < 0 || inputIndex >= InputGates.Count)
throw new ArgumentOutOfRangeException(nameof(inputIndex));
InputGates[inputIndex].ConnectInputWithOutput(inputGenerator);
}
}
Тут сразу стоит пояснить, что весь сакральный смысл состоит в том, что каждый вход и выход пользовательского вентиля тоже является вентилем (DirectGate). Благодаря этому мы можем сделать связи как на картинке:
Как видно из картинки, мы подключаем генератор (GEN) не напрямую к какому-то из внутренних вентелей (AND/NOT), а к входным вентилям (0/1), которые потом подключаются к внутренним (AND/NOT). Аналогично с выходом (R)
Пример из картинки в виде кода:
var and = new AndGate(); // создали блок AND
var not = new NotGate(); // создали блок NOT
not.ConnectInputWithOutput(and); // подключили вход NOT к выходу AND
var nand = new UserGate(inputsCount: 2); // создали блок NAND с 2-мя входами
nand.OutputGate.ConnectInputWithOutput(not); // поключили выход NAND к выходу NOT
and.ConnectInputWithOutput(0, nand.InputGates[0]); // подключаем 0-й вход AND к 0-му входу NAND
and.ConnectInputWithOutput(1, nand.InputGates[1]); // подключаем 1-й вход AND к 1-му входу NAND
// подключаем входные сигналы NAND к генератору
nand.InputGates[0].ConnectInputWithOutput(TrueGenerator.Instance);
nand.InputGates[1].ConnectInputWithOutput(TrueGenerator.Instance);
Console.WriteLine(nand); // (1, 1) ---UserGate--> 0
// отключаем входные сигналы NAND от генератора
//nand.InputGates[0].DisconnectInputGenerator();
//nand.InputGates[1].DisconnectInputGenerator();
//Console.WriteLine(nand); // (0, 0) ---UserGate--> 1
Протестировать онлайн можно тут.
СОХРАНЕНИЕ
Я не реализовывал сериализацию, но я думаю она вполне возможна с моим кодом. Я это вижу так:
- Добавить каждому генератору/вентилю свойство
Guidдля хранения его уникального идентификатора в пределах схемы - Предположим, что мы работаем с System.Text.Json, тогда для классов
ILogicGateнужно сделать конвертер, которые бы превращал все свои инпуты в массивGuidи наоборот. Для того чтобы работала десериализация, нужно будет изменить списокIReadOnlyList<ISignalGenerator> InputGeneratorsна изменяемый. В идеале стоит сделать свой список, чтобы можно было безопасно изменять значения с проверкой наnull.


