C#/게임 제작 + TIL

함수 포인터 (Action , func)

Toa_ 2025. 1. 31. 17:38

인 게임 예시 이미지

 

 

앞서 포스팅한 글은 몬스터의 스킬을 Queue 에 대기시킨 뒤  Skill()을 호출하여 (모션+스킬효과가) 즉시 사용이 되는 방식으로 몬스터의 공격을 구현하였다.

 

<이전에 사용한 코드>

async UniTask Skill(CancellationToken token)
{
    try
    {
        // 스킬 사용 전 랜덤 딜레이
        float delay = UnityEngine.Random.Range(0.2f, 1.2f);
        await UniTask.Delay(TimeSpan.FromSeconds(delay), cancellationToken: token);

        if (token.IsCancellationRequested) return; // 토큰이 취소되었으면 실행 중단

        // 스킬 발동 (실제 사용,애니메이션, SFX 등)
        Debug.Log($"{gameObject.name} uses {skillQue[0].skillName}");
        skillQue[0].UseSkill();
	//...
    }
}

 

 

 

하지만 실제 게임에서는 스킬사용을 요청해도 즉시 스킬의 효과가 적용되는것이 아니라

 

스킬의 시전 모션 >> 스킬 시전 + 스킬 효과 적용

 

같은 중간 단계가 존재하는데, 이 때문에 사전에 제작해 두었던 몬스터의 공격 모션 도중 몬스터가 공격하는 시점에서 스킬의 FX 와 효과가 적용되는 구조로 만들고 싶었다.

 

이를 위해서 먼저 skillQue[0].UseSkill(); 부분이 원하는 시점에서 사용되도록 만들어야 했고, 해당 방식을 위해서 새로운 방식을 찾아보았다.

 

 

 

 

1. Action

 

새로운 구현 방식을 찾던 중 델리게이트의 한 가지 형태인 Action을 알게 되었다.

Action은 리턴값이 없는 메서드 형식으로 작동하며 간단히 설명하자면 Action 으로 선언된 델리게이트에 직접 원하는 기능, 함수를 넣고, action 으로 선언된 델리게이트 자체가 메서드로서 기능을 하는 형태이다.

 

이때 선언되는 action은 매개변수를 받아서 작동할 수 있고, 매개변수가 없이 대입받은 메서드자체의 기능만 실행하는 것도 가능하다.

아래는 예시이다.

public class MyClass
{
    public static void PrintHello()
    {
        Console.WriteLine("Hello!");
    }

    public static void PrintSum(float a, float b)
    {
        Console.WriteLine("Sum: " + (a + b));
    }
}

public class MainClass
{
    public static void Main()
    {
        Action helloAction = MyClass.PrintHello;
        helloAction(); //매개변수 없이

        Action<float, float> sumAction = MyClass.PrintSum;
        sumAction(2.5f, 3.5f); //매개변수 사용
    }
}

 

 

2. Func

 

이처럼 원하는 기능을 커스텀하여 새로운 형태의 메서드를 만들 수 있는 Action은 리턴값 없이 구현이 되지만, 이와는 반대로 리턴값을 가지는 Func 델리게이트가 존재하는데, 해당 대리자는 리턴값을 가지는 메서드를 받아서 구현된다.

아래는 예시이다.

public class MyClass
{
    public static int GetNumber()
    {
        return 42; //리턴값 존재
    }

    public static string ToString(int number)
    {
        return "Number: " + number; //리턴값 존재
    }
}

public class MainClass
{
    public static void Main()
    {
        Func<int> numberFunc = MyClass.GetNumber;
        int number = numberFunc(); //리턴값 활용
        Console.WriteLine("Number: " + number);

        Func<int, string> toStringFunc = MyClass.ToString;
        string result = toStringFunc(42); //매개변수를 받아 리턴
        Console.WriteLine(result);
    }
}

 

 

 

주의할 점

 

여기서 자세히 보면 Action 에서는 사용하는 매개변수를 <@@>에 표현해 주고,  Func 에서는 다른 방식으로 <@@>를 표현해주고 있다. 이러한 차이가 생기는 이유는 반환형의 유무 때문에 생기는데.

 

Action는 보이는 그대로 대입받은 메서드에서 사용하는 매개변수를 그 개수와 타입에 맞추어 순서대로 <@@> 에 표시해 주며.

Func는 <매개변수 1, 매개변수 2,..., 반환형>의 형태로 맨 마지막에 반환형을 표시해 주는 식으로 표현한다.

 

따라서 numberFunc 의 경우 매개변수 없이 int 형 리턴값만 존재하여 Func<int> 가 되었고, toStringFunc 의 경우 int 형 매개변수를 받아 string을 리턴하기 때문에 Func<int, string> 이 되었다.

 

 

그럼 이러한 Action 과 Func의 기능을 배운 상태로 앞서 언급한 스킬의 구현법을 보면.  skillQue[0].UseSkill(); 을 원하는 시점에서 사용하고자 할 때 UseSkill()을 Action 대리자에 대입하여 필요한 시점에서 사용하면 된다는 것을 알 수 있다.

 

<수정된 코드>

public Action skillOnWaiting;
public MonsterSkillData[] skillQue;

 async UniTask Skill(CancellationToken token)
{
    try
    {
        //스킬 시전 애니메이션 재생....
        skillOnWaiting = () => 
        {
            skillQue[0].UseSkill(attackPower,skillFXObject);
        };
    }
    //...
}

 

public class SkillUseOnAnim : StateMachineBehaviour
{
    public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        MonsterObject monsterObject = animator.GetComponent<MonsterObject>();
        monsterObject.skillOnWaiting?.Invoke();//스킬의 fx, 데미지, 데미지 출력 실행.
        monsterObject.skillOnWaiting = null;
    }
}

 

이렇게 하면 스킬 시전 모션 중 정확히 데미지가 들어가는 시점에서 skillOnWaiting 대리자를 실행하여 원하는 기능을 구현할 수 있다.

 

여기서 중요한 것은 = null;을 통해 skillOnWaiting을 명시적으로 초기화해서 대리자가 중복된 행동을 하지 않도록 하는 것이다.