Created
November 23, 2025 10:36
-
-
Save adammyhre/2f11617426039304973eda2acedb752b to your computer and use it in GitHub Desktop.
Generic Processing Chains
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using UnityEngine; | |
| public interface IProcessor<in TIn, out TOut> { | |
| TOut Process(TIn input); | |
| } | |
| public delegate TOut ProcessorDelegate<in TIn, out TOut>(TIn input); | |
| public class ThresholdFilter : IProcessor<float, bool> { | |
| readonly Func<float> getThreshold; | |
| public ThresholdFilter(Func<float> getThreshold) { | |
| this.getThreshold = getThreshold; | |
| } | |
| public bool Process(float score) => score >= getThreshold(); | |
| } | |
| public class DistanceScorer : IProcessor<float, float> { | |
| public float Process(float distance) => 1f / (1f + distance); | |
| } | |
| public class DistanceFromPlayer : IProcessor<Vector3, float> { | |
| readonly Transform player; | |
| public DistanceFromPlayer(Transform player) { | |
| this.player = player; | |
| } | |
| public float Process(Vector3 point) => Vector3.Distance(player.position, point); | |
| } | |
| class ClampByMaxDistance : IProcessor<float, float> { | |
| readonly float maxDistanceScoreThreshold; | |
| public ClampByMaxDistance(float maxDistance) { | |
| maxDistanceScoreThreshold = 1f / (1f + maxDistance); | |
| } | |
| public float Process(float score) => score < maxDistanceScoreThreshold ? 0f : score; | |
| } | |
| public class HighlightIfClose : IProcessor<bool, bool> { | |
| readonly Transform transform; | |
| public HighlightIfClose(Transform target) => transform = target; | |
| public bool Process(bool isCloseEnough) { | |
| // TODO cache propertyblock and null check | |
| var renderer = transform.GetComponent<Renderer>(); | |
| renderer.material.color = isCloseEnough ? Color.red : Color.white; | |
| return isCloseEnough; | |
| } | |
| } | |
| class Combined<A, B, C> : IProcessor<A, C> { | |
| readonly IProcessor<A, B> first; | |
| readonly IProcessor<B, C> second; | |
| public Combined(IProcessor<A, B> first, IProcessor<B, C> second) { | |
| this.first = first; | |
| this.second = second; | |
| } | |
| public C Process(A input) => second.Process(first.Process(input)); | |
| } | |
| class Chain<TIn, TOut> { | |
| readonly IProcessor<TIn, TOut> processor; | |
| Chain(IProcessor<TIn, TOut> processor) { | |
| this.processor = processor; | |
| } | |
| public static Chain<TIn, TOut> Start(IProcessor<TIn, TOut> processor) { | |
| return new Chain<TIn, TOut>(processor); | |
| } | |
| public Chain<TIn, TNext> Then<TNext>(IProcessor<TOut, TNext> next) { | |
| var combined = new Combined<TIn, TOut, TNext>(processor, next); | |
| return new Chain<TIn, TNext>(combined); | |
| } | |
| public TOut Run(TIn input) => processor.Process(input); | |
| public ProcessorDelegate<TIn, TOut> Compile() => input => processor.Process(input); // Turns the pipeline into a reusable function | |
| } | |
| // ────────────────────────────────────────────────────────────── | |
| // Advanced Fluent Chain API | |
| // ────────────────────────────────────────────────────────────── | |
| public static class Chain { | |
| public static DistanceChain FromPlayer(Transform player) => new DistanceChain(new DistanceFromPlayer(player)); | |
| public static DistanceChain Start(DistanceFromPlayer processor) => new DistanceChain(processor); | |
| // public static DistanceChain Start<TProcessor>(TProcessor processor) where TProcessor : IProcessor<Vector3, float> => new DistanceChain(processor); | |
| } | |
| // ────────────────────────────────────────────────────────────── | |
| // Fluent Chain Base Class | |
| // Without TDerived in the base, C# would not allow a derived class | |
| // to add a method that returns a more specific type than the base returns. | |
| // ────────────────────────────────────────────────────────────── | |
| public abstract class FluentChain<TIn, TOut, TDerived> where TDerived : FluentChain<TIn, TOut, TDerived> { | |
| public IProcessor<TIn, TOut> processor; | |
| protected FluentChain(IProcessor<TIn, TOut> processor) { | |
| this.processor = processor ?? throw new ArgumentNullException(nameof(processor)); | |
| } | |
| protected TNextSelf Then<TNext, TNextSelf, TProcessor>( | |
| TProcessor nextProcessor, | |
| ChainFactory<TIn, TNext, TNextSelf> factory) | |
| where TNextSelf : FluentChain<TIn, TNext, TNextSelf> | |
| where TProcessor : class, IProcessor<TOut, TNext> | |
| { | |
| if (nextProcessor == null) throw new ArgumentNullException(nameof(nextProcessor)); | |
| if (factory == null) throw new ArgumentNullException(nameof(factory)); | |
| return factory(new Combined<TIn, TOut, TNext>(processor, nextProcessor)); | |
| } | |
| public TOut Run(TIn input) { | |
| if (processor == null) throw new InvalidOperationException("Processor is not initialized. Use Chain.Start() to begin a chain."); | |
| return processor.Process(input); | |
| } | |
| public ProcessorDelegate<TIn, TOut> Compile() { | |
| if (processor == null) throw new InvalidOperationException("Processor is not initialized. Use Chain.Start() to begin a chain."); | |
| return input => processor.Process(input); | |
| } | |
| } | |
| // ────────────────────────────────────────────────────────────── | |
| // Factory delegate type for creating chain instances | |
| // ────────────────────────────────────────────────────────────── | |
| public delegate TChain ChainFactory<out TIn, in TOut, out TChain>(IProcessor<TIn, TOut> processor) | |
| where TChain : FluentChain<TIn, TOut, TChain>; | |
| // ────────────────────────────────────────────────────────────── | |
| // Concrete Chain Types | |
| // ────────────────────────────────────────────────────────────── | |
| public class DistanceChain : FluentChain<Vector3, float, DistanceChain> { | |
| public DistanceChain(IProcessor<Vector3, float> processor) : base(processor) { } | |
| static ScoredChain CreateScoredChain(IProcessor<Vector3, float> processor) { | |
| return new ScoredChain(processor); | |
| } | |
| public ScoredChain Then<TProcessor>(TProcessor scorer) where TProcessor : class, IProcessor<float, float> | |
| => Then<float, ScoredChain, TProcessor>(scorer, CreateScoredChain); | |
| } | |
| public class ScoredChain : FluentChain<Vector3, float, ScoredChain> { | |
| public ScoredChain(IProcessor<Vector3, float> processor) : base(processor) { } | |
| static FilteredChain CreateFilteredChain(IProcessor<Vector3, bool> processor) { | |
| return new FilteredChain(processor); | |
| } | |
| public ScoredChain WithMaxDistance(float maxDist) { | |
| processor = new Combined<Vector3, float, float>(processor, new ClampByMaxDistance(maxDist)); | |
| return new ScoredChain(processor); | |
| } | |
| public FilteredChain Then<TProcessor>(TProcessor filter) where TProcessor : class, IProcessor<float, bool> | |
| => Then<bool, FilteredChain, TProcessor>(filter, CreateFilteredChain); | |
| } | |
| public class FilteredChain : FluentChain<Vector3, bool, FilteredChain> { | |
| public FilteredChain(IProcessor<Vector3, bool> processor) : base(processor) { } | |
| static FilteredChain Create(IProcessor<Vector3, bool> processor) { | |
| return new FilteredChain(processor); | |
| } | |
| public FilteredChain LogTo(string system) { | |
| Debug.Log($"#{system}# Filtered Chain!"); | |
| return this; | |
| } | |
| public FilteredChain Then<TProcessor>(TProcessor next) where TProcessor : class, IProcessor<bool, bool> | |
| => Then<bool, FilteredChain, TProcessor>(next, Create); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment