Last active
November 17, 2025 06:34
-
-
Save adammyhre/747371bc14470f3b31fdbb62d758e9bd to your computer and use it in GitHub Desktop.
Curiously Recurring Template Pattern (CRTP) in C#
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 UnityEngine; | |
| abstract class Base<TDerived> where TDerived : Base<TDerived> { | |
| public void DoWork() { | |
| ((TDerived)this).Work(); // Call derived class method, strongly typed | |
| } | |
| public TDerived SetName(string value) { | |
| Debug.Log($"#Base# Setting name to {value}"); | |
| return (TDerived)this; // Fluent interface | |
| } | |
| protected abstract void Work(); | |
| } | |
| class Worker : Base<Worker> { | |
| protected override void Work() { | |
| UnityEngine.Debug.Log("#Worker# Worker is doing work."); | |
| } | |
| } |
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
| abstract class Cloneable<TDerived> where TDerived : Cloneable<TDerived>, new() { | |
| public TDerived Clone() { | |
| return new TDerived(); | |
| } | |
| } | |
| class Enemy : Cloneable<Enemy> { } |
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
| abstract class Builder<TDerived> where TDerived : Builder<TDerived> { | |
| protected string name; | |
| public TDerived SetName(string value) { | |
| name = value; | |
| return (TDerived)this; // returns the *derived* type | |
| } | |
| } | |
| class EnemyBuilder : Builder<EnemyBuilder> { | |
| int health; | |
| public EnemyBuilder SetHealth(int value) { | |
| health = value; | |
| return this; | |
| } | |
| public Enemy Build() { | |
| return new Enemy(name, health); | |
| } | |
| } |
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.Collections.Generic; | |
| using UnityEngine; | |
| public class MoveState : CharacterState<MoveState> { | |
| protected override void OnEnter() { | |
| Debug.Log("#State# Entered Move"); | |
| } | |
| protected override void OnExit() { | |
| Debug.Log("#State# Exited Move"); | |
| } | |
| protected override void OnTick(float dt) { | |
| // Movement logic... | |
| // ConsoleProDebug.Watch("DeltaTime in Move State", dt.ToString()); | |
| } | |
| } | |
| public class IdleState : CharacterState<IdleState> { | |
| protected override void OnEnter() { | |
| Debug.Log("#State# Entered Idle"); | |
| } | |
| protected override void OnExit() { | |
| Debug.Log("#State# Exited Idle"); | |
| } | |
| protected override void OnTick(float dt) { | |
| // Idle logic... | |
| } | |
| } | |
| public class CharacterStateMachine { | |
| CharacterState current; | |
| public void ChangeState<TState>(TState newState) where TState : CharacterState<TState> { | |
| current?.Exit(); | |
| current = newState; | |
| current.Enter(); | |
| } | |
| public void Tick(float dt) { | |
| // current?.Tick(dt); | |
| if (current == null) return; | |
| var t = current.GetTransition(); | |
| if (t != null) { | |
| t.Apply(this); | |
| return; | |
| } | |
| current.Tick(dt); | |
| } | |
| } | |
| public abstract class CharacterState<TState> : CharacterState where TState : CharacterState<TState> { | |
| // Methods in here can explicitly use `(TState)this`. | |
| // This is especially useful if this class needs to return a value of TState. | |
| public override void Enter() { | |
| ((TState)this).OnEnter(); | |
| } | |
| public override void Exit() { | |
| ((TState)this).OnExit(); | |
| } | |
| public override void Tick(float dt) { | |
| ((TState)this).OnTick(dt); | |
| } | |
| protected abstract void OnEnter(); | |
| protected abstract void OnExit(); | |
| protected abstract void OnTick(float dt); | |
| } | |
| public abstract class CharacterState { | |
| readonly List<Transition> transitions = new List<Transition>(); | |
| public Transition GetTransition() { | |
| for (int i = 0; i < transitions.Count; i++) { | |
| if (transitions[i].Evaluate()) return transitions[i]; | |
| } | |
| return null; | |
| } | |
| public void SetTransition<TState>(Transition<TState> t) where TState : CharacterState<TState> { | |
| transitions.Add(t); | |
| } | |
| public abstract void Enter(); | |
| public abstract void Exit(); | |
| public abstract void Tick(float dt); | |
| } |
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; | |
| public abstract class Transition { | |
| public abstract bool Evaluate(); | |
| public abstract void Apply(CharacterStateMachine machine); | |
| } | |
| public class Transition<TState> : Transition where TState : CharacterState<TState> { | |
| readonly Func<bool> condition; | |
| readonly TState target; | |
| public Transition(TState target, Func<bool> condition) { | |
| this.target = target; | |
| this.condition = condition; | |
| } | |
| public override bool Evaluate() => condition(); | |
| public override void Apply(CharacterStateMachine machine) { | |
| machine.ChangeState(target); // Type-safe generic dispatch | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment