티스토리 뷰


🕹️ 실습 (2D 횡스크롤 스테이트머신)

Attack

// Entity.cs

[Header("Collision Info")]
public Transform attackCheck;
public float attackCheckRadius;

protected virtual void OnDrawGizmos()
{
    Gizmos.DrawWireSphere(attackCheck.position, attackCheckRadius);
}

public virtual void TakeDamage()
{
    Debug.Log($"{gameObject} gets damage");
}
// PlayerAnimationTriggers.cs

private void AttackTrigger()
{
    Collider2D[] colliders = Physics2D.OverlapCircleAll(player.attackCheck.position, player.attackCheckRadius); // 범위 안에 있는 모든 Collider를 가져옴

    foreach(var hit in colliders)
    {
        if (hit.GetComponent<Enemy>() != null)
        {
            hit.GetComponent<Enemy>().TakeDamage();
        }
    }
}

Damage Effect

  • Material 생성 후 GUI/TextShader로 변경한다
  • Player Animator의 Material에 넣어준다
  • Player에 EntityFX 스크립트를 붙여준 후 FlashFXMaterial을 넣어준다
public class EntityFX : MonoBehaviour
{
    private SpriteRenderer sr;

    [Header("Flash FX")]
    [SerializeField] private Material hitMat;
    private Material originMat;

    void Start()
    {
        sr = GetComponentInChildren<SpriteRenderer>();
        originMat = sr.material;
    }

    private IEnumerator FlashFX()
    {
        sr.material = hitMat;
        yield return new WaitForSeconds(0.2f);
        sr.material = originMat;
    }

}
// Entity.cs

public EntityFX fx { get; private set; }

protected virtual void Start()
{
    fx = GetComponent<EntityFX>();
}

public virtual void TakeDamage()
{
    fx.StartCoroutine("FlashFX");
    Debug.Log($"{gameObject.name} took damage");
}

Knockback

// Entity.cs

[Header("Knockback Info")]
[SerializeField] protected Vector2 knockbackDirection;
[SerializeField] private float knockbackDuration;
protected bool isKnocked;

public virtual void TakeDamage()
{
    fx.StartCoroutine("FlashFX");
    StartCoroutine("HitKnockBack");
    Debug.Log($"{gameObject.name} took damage");
}

protected virtual IEnumerator HitKnockBack()
{
    isKnocked = true;
    rb.linearVelocity = new Vector2(knockbackDirection.x * -facingDir, knockbackDirection.y);
    yield return new WaitForSeconds(knockbackDuration);
    isKnocked = false;
}

public void SetZeroVelocity()
{
    if (isKnocked)
    {
        return;
    }

    rb.linearVelocity = new Vector2(0, 0);
}

public void SetVelocity(float _xVelocity, float _yVelocity)
{
    if (isKnocked)
    {
        return;
    }

    rb.linearVelocity = new Vector2(_xVelocity, _yVelocity);
    HandleFlip(_xVelocity);
}

Stun

public class SkeletonStunnedState : EnemyState
{
    protected Skeleton enemy;

    public SkeletonStunnedState(Enemy _enemyBase, EnemyStateMachine _stateMachine, string _animBoolName, Skeleton _enemy)
        : base(_enemyBase, _stateMachine, _animBoolName)
    {
        enemy = _enemy;
    }

    public override void Eneter()
    {
        base.Eneter();

        stateTimer = enemy.stunDuration;

        rb.linearVelocity = new Vector2(-enemy.facingDir * enemy.stunDirection.x, enemy.stunDirection.y);
    }

    public override void Update()
    {
        base.Update();

        enemy.fx.InvokeRepeating("RecolorBlink", 0, 0.1f);

        if (stateTimer < 0)
        {
            stateMachine.ChangeState(enemy.idleState);
        }
    }

    public override void Exit()
    {
        base.Exit();
        enemy.fx.Invoke("CancelRedBlink", 0);
    }

}
// EntityFX.cs

private void RecolorBlink()
{
    sr.color = (sr.color != Color.white) ? Color.white : Color.red;
}

private void CancelRedBlink()
{
    CancelInvoke();
    sr.color = Color.white;
}

protected override void Awake()
{
    counterAttackState = new PlayerCounterAttackState(this, stateMachine, "CounterAttack");
}

Counter Attack과 Stun

// Player.cs
public float counterAttackDuration = 0.2f;

public PlayerCounterAttackState counterAttackState { get; private set; }
// PlayerGroundedState.cs

if (Input.GetKeyDown(KeyCode.M))
{
    stateMachine.ChangeState(player.counterAttackState);
}
public class PlayerCounterAttackState : PlayerState
{
    public PlayerCounterAttackState(Player _player, PlayerStateMachine _stateMachine, string _animBoolName)
        : base(_player, _stateMachine, _animBoolName)
    {
    }

    public override void Enter()
    {
        base.Enter();

        stateTimer = player.counterAttackDuration;
        player.anim.SetBool("SucceedCounterAttack", false);
    }

    public override void Update()
    {
        base.Update();

        Collider2D[] colliders = Physics2D.OverlapCircleAll(player.attackCheck.position, player.attackCheckRadius);

        foreach (var hit in colliders)
        {
            if (hit.GetComponent<Enemy>() != null)
            {
                if (hit.GetComponent<Enemy>().CanBeStunned())
                {
                    stateTimer = 10;
                    player.anim.SetBool("SucceedCounterAttack", true);
                }
            }
        }

        if (stateTimer < 0 || triggerCalled) // 시간 내에 못 쳤을 경우 idle로 가라
        {
            stateMachine.ChangeState(player.idleState);
        }
    }

    public override void Exit()
    {
        base.Exit();
        player.anim.SetBool("SucceedCounterAttack", false);
    }
}
// Enemy.cs

public virtual void OpenCounterAttackWindow() // Animation Event와 연결
{
    canBeStunned = true;
    counterImage.SetActive(true);
}

public virtual void CloseCounterAttackWindow() // Animation Event와 연결
{
    canBeStunned = false;
    counterImage.SetActive(false);
}

public virtual bool CanBeStunned()
{
    if (canBeStunned)
    {
        CloseCounterAttackWindow();
        return true;
    }
    return false;
}
// Skeleton.cs

public override bool CanBeStunned()
{
    if (base.CanBeStunned())
    {
        stateMachine.ChangeState(stunnedState);
        return true;
    }

    return false;
}

Enemy가 canBeStunned가 true일 때 Player가 반격하면, Enemy가 Stun상태로 된다

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/10   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함