Damiano Tagliaferri

Ice & Fire

Jan 5, 2024
Cover art
Shield ImageShield ImageShield Image

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

My contributions

  1. Gameplay programming
  2. Custom combat system
  3. Checkpoint system

Gameplay

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.

Combat system

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();
    }

    // ...

}
© 2024 Damiano Tagliaferri