Personally I would use async paradigm (using the awesome UniTask library) like @lordofduct suggested, combined with a TurnSystem that finds and execute all processors you created, something like this:
public interface ITurnProcessor
{
int ExecutionOrder { get; }
UniTask<TurnProcessingResult> ProcessTurnAsync(TurnContext context);
}
public enum TurnProcessingResult
{
Success,
Failed
}
// A data class shared between all processors, like a Blackboard,
// where a processor can access, modify or set data for the next processors to use
public TurnContext
{
public Player Player { get; set; }
public List<UnitsDestinations> UnitDestinations { get; set; }
public AnyOtherNeededProperty { get; set; }
}
Some examples of TurnProcessors:
public class MoveUnitsTurnProcessor : ITurnProcessor
{
// ITurnProcessor implementation
public int ExecutionOrder => 0;
// ITurnProcessor implementation
public async UniTask<TurnProcessorResult> ProcessTurnAsync(TurnContext context)
{
// Move each unit, one by one, to its destination.
// if we want to move them all at the same time,
// we use await UniTask.WhenAll() to run all tasks
// in parallel
foreach (var unitDestination in context.UnitsDestinations)
{
await MoveUnitAsync(unitDestination);
}
return TurnProcessorResult.Success;
}
private async UniTask MoveUnitAsync(UnitDestination unitDestination)
{
var unit = UnitDestination.Unit;
var unitTransform = unit.Transform;
var destination = UnitDestination.Destination;
// Set unit destination (using NavMeshAgent for example...)
unit.SetDestination(destination);
// awaits until the Unit has reached its destination
await UniTask.Until(() => Vector3.Distance(unitTransform.position, destination) < 0.05f);
}
}
// UI/Event based TurnProcessor
public class WaitForUserSelectionTurnProcessor : MonoBehaviour, ITurnProcessor
{
// Used to convert a normal event to Async...
private TaskCompletionSource<TurnProcessingResult> _clickTaskCompletionSource;
private TurnContext _turnContext;
[SerializeFiled] private GameObject _selectionCanvas;
[SerializeField] private Button[] _buttons;
private void Awake()
{
foreach (var button in _buttons)
button.Click += OnButtonClick;
}
private void OnButtonClick()
{
// button clicked, do some actions...
// Set the async TCS result...
_clickTaskCompletionSource.TrySetResult(TurnProcessingResult.Success);
}
// ITurnProcessor implementation
public int ExecutionOrder => 1;
// ITurnProcessor implementation
public async UniTask<TurnProcessingResult> ProcessTurnAsync(TurnContext context)
{
// Show the buttons canvas...
_selectionCanvas.SetActive(true);
// Create the TCS:
_clickTaskCompletionSource = new TaskCompletionSource<TurnProcessingResult>();
// Set the current TurnContext, which can be used in the buttons click event
_turnContext = context;
// Awaits and get the TCS result, set from the buttons click event callback
var result = await _clickTaskCompletionSource.Task;
// Hide the buttons canvas:
_selectionCanvas.SetActive(false);
// Return the result
return result;
}
}
The TurnSystem:
public class TurnSystem : Singleton<TurnSystem>
{
private readonly IReadOnlyList<ITurnProcessor> _allTurnProcessors = GetAllTurnProcessors();
public async UniTask<bool> ExecuteTurnAsync(TurnContext context)
{
foreach (var processor in _allTurnProcessors)
{
var result = await processor.ProcessTurnAsync(context);
if (result != TurnProcessingResult.Success)
{
// One of the processors failed, we just stop executing turns for example
return false;
}
return true;
}
}
private static IReadOnlyList<ITurnProcessor> GetAllTurnProcessors
{
// We used reflection here. We could also make processors ScriptableObjects
// and assign them directly from the Editor, which will nicely solve
// the execution order problem too.
var result = new List<ITurnProcessor>();
var processorsParent = new GameObject("Turn Processors (MonoBehaviours)");
var interfaceType = typeof(ITurnProcessor);
var allProcessorsTypes = interfaceType.Assembly.GetTypes()
.Where(x => !x.IsAbstract && interfaceType.IsAssignableFrom(x));
foreach (var processorType in allProcessorsTypes)
{
// if it's a MonoBehaviour, we create a game object.
// We could also do a FindObjectOfType if the object is already in the scene.
if (typeof(MonoBehaviour).IsAssignableFrom(processorType))
{
var go = new GameObject(processorType.Name);
go.transform.SetParent(processorsParent.transform);
result.Add((ITurnProcessor) go.AddComponent(processorType));
}
else
{
result.Add((ITurnProcessor) Activator.CreateInstance(processorType));
}
}
return result.OrderBy(x => x.ExecutionOrder).ToList();
}
}
Finally, in your “Execute Turn” button, you just do:
private async void ButtonClick()
{
// Fill your turn context...
var context = new TurnContext
{
};
var turnSuccessful = await TurnSystem.Instance.ExecuteTurnAsync(context);
}