Vertical slice for a distinctive top-down hack & slash game, where the focus shifts from limiting damage taken to balancing your character’s temperature and maintaining it at a stable level.
Role: Lead Developer
Company: <Self employed>
Platforms: PC
Work time: 2 years
- Gameplay programming
- Custom combat system
- Checkpoint system
I’m responsible for implementing main character locomotion and abilities, while ensuring they play well with animations and interact correctly with other systems. These include a controller stick aim assist system and enemy lock in melee combat.
Developed an async state machine that greatly simplifies creation of AI behaviours.
AI programming
This is how Idle and Chase states for a simple enemy would look like:
public class CustomEnemyAI : BaseEnemyAI
{
protected override FSMState EnterState => IdleState;
async UniTask<FSMState> IdleState(CancellationToken token)
{
await HTask.WaitUntil(() => Actor.CanSeePlayer, token);
return ChaseState;
}
async UniTask<FSMState> ChaseState(CancellationToken token)
{
await HTask.RepeatUntil(
() => Agent.SetDestination(PlayerInstance.Position),
() => Actor.CanAttackPlayer || !() => Actor.CanSeePlayer,
.1f, // Loop delta time
token
);
return Actor.CanAttackPlayer ? AttackState : ReturnToBoundsState;
}
// ...
}
UniTask FSM
/// <summary>
/// Base class for enemy artificial intelligence.
/// </summary>
[RequireComponent(typeof(AEnemy))]
public abstract partial class BaseEnemyAI : MonoBehaviour
{
public delegate UniTask<FSMState> FSMState(CancellationToken cancellationToken);
protected FSMState CurrentState;
protected abstract FSMState EnterState { get; }
private CancellationTokenSource InterruptTokenSource { get; set; }
// ...
protected virtual void Start()
{
// ...
CurrentState = EnterState;
_ = StartFSMAsync();
}
#region FSM Loop
private async UniTask StartFSMAsync()
{
var linkedTokenSource = GenerateLinkedTokenSource();
try
{
await AILoopAsync(linkedTokenSource.Token);
}
catch (OperationCanceledException ex)
{
Debug.Log($"AILoop was cancelled: {ex.Message}", gameObject);
}
finally
{
InterruptTokenSource?.Dispose();
InterruptTokenSource = null;
}
}
private async UniTask AILoopAsync(CancellationToken token)
{
while (true)
{
if (logCurrentState) LogCurrentState();
CurrentState = await CurrentState(token);
}
}
private CancellationTokenSource GenerateLinkedTokenSource()
{
InterruptTokenSource ??= new CancellationTokenSource();
return CancellationTokenSource.CreateLinkedTokenSource(
this.GetCancellationTokenOnDestroy(),
InterruptTokenSource.Token
);
}
#endregion
/// <summary>
/// Interrupts the current AI state and transitions to the specified state.
/// </summary>
/// <param name="nextState">The next state to transition to.</param>
protected void InterruptStateWith(FSMState nextState)
{
InterruptTokenSource.Cancel();
OnStateInterrupted();
CurrentState = nextState;
_ = StartFSMAsync();
}
// ...
}