관리 메뉴

개발블로그

[유니티 C#] Action 과 Func 본문

Unity/스크립팅

[유니티 C#] Action 과 Func

dvmoo 2021. 9. 4. 12:54

💪 GOAL

https://devheehee.tistory.com/21

지난 포스팅에서 Delegate의 사용 방법을 알아보았다.

지난 시간에 이어서 C#에서 Delegate를 좀 더 간단히 사용할 수 있도록 미리 선언한 Delegate 변수인 

Action과 Func의 차이점을 알아보자.

 

- C#에서의 델리게이트 기능을 손쉽게 사용할 수 있도록 지원하는 Action과 Func에 대해 이해한다.


🔶 Action

 Action 타입은 입력과 출력이 없는 메소드를 가리킬 수 있는 델리게이트다.

입력과 출력이 없는 메소드를 등록할 수 있고, 등록된 메소드는 원하는 시점에 매번 실행할 수 있다.

 

예제 코드를 보자.

public class Monster : Monobehaviour
{
	Action attack;
    
    void Start()
    {
    	attack += Punch;
        attack += Kick;
    }
    
    void Update()
    {
    	if(Input.GetMouseButtonDown(0))
        {
        	attack();
        }
    }
    
    void Punch()
    {
    	Debug.Log("Punch!");
    }
	
    void Kick()
    {
    	Debug.Log("Kick!");
    }
}

Action 타입의 변수에는 += 를 사용해서 메소드를 등록할 수 있다.

이 예제 코드는 마우스 왼쪽 버튼을 누를 때마다 attack(); 을 실행하게 된다.

attack(); 이 실행되면 attack에 등록된 Punch()와 Kick() 이 실행되어 'Punch!' 와 'Kick!' 로그가 순서대로 출력된다.

 

 

🔶 Action과 Func, 그리고 그 둘의 차이점

✔ Action<T>     

Action<T> 에서 T자리에 n개의 type을 입력하면 그 type들이 전송가능한 매개변수로 자동 인식된다.

public delegate void Action();		// 매개변수가 없으며 값을 반환하지 않는 메소드를 캡슐화
public delegate void Action<in T>(T obj);
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);
.
.
.
public delegate void Action<in T1,in T2,in T3,in T4,in T5,in T6,in T7,in T8,in T9,in T10,in T11,in T12,in T13,in T14,in T15,in T16>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15, T16 arg16);

Action은 위와 같은 선언부가 숨어있다.

매개변수가 16개까지 미리 선언되어 있어서 리턴값이 없는 델리게이트 사용이 필요할 때 Action을 통해 기능을 사용할 수 있다.

 

예제코드를 보자.

using UnityEngine;
using System;

public class Study_Action : MonoBehaviour
{
    Action<int, float> _action;    // int와 float을 매개변수로 받고, 리턴값이 없는 이벤트

    void Start()
    {
        _action += PrintAction;

        _action?.Invoke(1, 1.0f);
    }

    void PrintAction(int a, float b)
    {
        Debug.Log("int a = " + a + " float b = " + b + "f" );
    }
}

1. Action을 선언한 뒤 매개변수로 int와 float을 받은 _action을 선언했다.

2. 스크립트가 실행되면 _action 대리자에 PrintAction 함수를 넣어준다. (델리게이트 체인)

3. _action 대리자가 null이면 null을 반환하고 null이 아니면 대리자 내부의 메소드를 실행한다.

   매개변수 int와 float을 넣어준다.

 

그림 1 실행 결과

PrintAction()_action 대리자에 체인해주었으므로

_action?.Invoke(1, 1.0f); 를 실행하면 _action이 캡슐화하고있는 메소드를 실행한다.

 

✔ Func<TResult>

Func<TResult>는 리턴값이 필요한 Event이다.

Func<TResult> 에서 TResult는 반환값이 들어간다.

만약 매개변수가 있다면 Func<T1, TResult>의 형식으로 마지막 파라미터는 반환값의 type을 넣어줘야 한다.

public delegate TResult Func<out TResult>();	// 매개변수가 없으며 값을 반환하는 메소드를 캡슐화
public delegate TResult Func<in T, out TResult>(T arg);
.
.
.
public delegate TResult Func<in T1,in T2,in T3,in T4,in T5,in T6,in T7,in T8,in T9,in T10,in T11,in T12,in T13,in T14,in T15,in T16,out TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15, T16 arg16);

Func는 위와 같은 선언부가 숨어있다.

Action과 마찬가지로 매개변수가 16개까지 미리 선언되어있고, 마지막 파라미터는 반환 값의 type을 넣어준다.

 

형식 매개변수 T

- 매개 변수 형식으로 이 형식 매개변수이다.

 

매개변수 obj

- 이 대리자로 캡슐화 되는 메소드의 매개변수.

 

형식 매개변수 TResult (리턴값이 있는 Func 에서 사용)

- 이 대리자로 캡슐화 되는 메소드의 반환 값 형식으로 이 형식 매개변수이다.

 

 


* Action 예제

메인화면에 기본 자동차를 띄우고 Action을 이용해서 리스트에 나타난 자동차슬롯을 누르면 로그를 출력하도록 작성했다.

 

GIF 1

메인화면에서는 기본 자동차가 회전하고 Select 버튼을 누르면 자동차 리스트가 나타난다.

자동차 리스트에서 슬롯을 선택하면 "input change car" 로그가 출력된다.

 

코드를 보자.

 

CarType.cs

public enum ECarType
{
    None,

    BlueCar,
    GreenCar,
    Hiace,
    RedBus,
    BlueTruck,
    TukTuk,
    YellowBus,

    Max,
}

실무에서는 엑셀에 데이터파일을 작성해서 파싱해 사용하지만 이번 예제에서는 그렇게 하지않고 자동차 종류를 enum문에 담았다.

 

여기서 None과 Max는 자동차의 종류는 아니지만

enum문 초기화를 위해서 None을 추가했고, for문을 돌리기 위해서 Max 를 추가했다. 가시성을 위함이다.

 

 

Panel_Main.cs

using UnityEngine;

public class Panel_Main : MonoBehaviour
{
    [SerializeField] GameObject _panelSelectCar = null;

    [SerializeField] Transform  _carParent      = null;

    GameObject _gbjCar = null;

    void Start()
    {
        Init();
    }
    
    void Update()
    {
        // Rotate Car
        _gbjCar.transform.Rotate(new Vector3(0f, 10f, 0f) * Time.deltaTime); 
    }

    void Init()
    {
        // Default Car
        _gbjCar = Instantiate((GameObject)Resources.Load("Cars/BlueCar"), _carParent);
    }

    public void OnBtnSelectCar()
    {
        _panelSelectCar.SetActive(true);
        _panelSelectCar.GetComponent<Panel_SelectCar>().Init(ChangeCar);

        _gbjCar.SetActive(false);
    }

    // 로그 출력
    public void ChangeCar(ECarType eType)
    {
        //change car
        Debug.Log("input change car");

    }
}

처음에 뜨는 Main 화면에서는 기본 자동차(BlueCar)를 생성하고, Update문에서 계속 회전시킨다.

 

Panel_SelectCar.cs

using System;
using UnityEngine;
using System.Collections.Generic;

public class Panel_SelectCar : MonoBehaviour
{
    [SerializeField] Transform _slotParent = null;

    [SerializeField] List<GameObject> _carList = new List<GameObject>();

    public void Init(Action<ECarType> onBtn)
    {
        for (int i = 0; i < _carList.Count; ++i)
        {
            var slot = Instantiate((GameObject)Resources.Load("Prefabs/Comp Slot"), _slotParent);

            slot.GetComponent<Comp_CarSlot>().SetSlot((ECarType)i+1, onBtn);
        }
    }
}

자동차리스트를 보여주는 프리팹에 붙어있는 Panel_SelectCar 스크립트이다.

 

그림 1 Panel SelectCar 프리팹

이 스크립트는 하이라키뷰를 보면 이해가 쉽다.

 

그림 2 Comp Slot 프리팹 

_slotParentComp Slot 프리팹을 생성하기 위해서 선언하고, 

자동차의 종류만큼 슬롯을 추가하기 위해 리스트 _carList를 선언했다.

 

Init() 에서는 리스트에 담긴 차의 종류만큼 for문을 돌면서 슬롯을 생성해준다.

슬롯을 생성한 후에는 Comp_CarSlot의 함수 SetSlot을 호출한다.

 

using System;
using UnityEngine;
using UnityEngine.UI;

public class Comp_CarSlot : MonoBehaviour
{
    [SerializeField] Transform  _carParent = null;
    GameObject _gbjCar = null;

    [SerializeField] Text _textName    = null;
    [SerializeField] Text _textExplain = null;

    ECarType _carType = ECarType.None;

    Action<ECarType> _onBtn;

    public void SetSlot(ECarType carType, Action<ECarType> onBtn)
    {
        _onBtn = onBtn;

        switch(carType)
        {
            case ECarType.BlueCar:
                {
                    _gbjCar = Instantiate((GameObject)Resources.Load("Cars/BlueCar"), _carParent);

                    _textName.text = "Blue car";
                    _textExplain.text = "파란차 입니다.";

                    break;
                }
            case ECarType.GreenCar:
                {
                    _gbjCar = Instantiate((GameObject)Resources.Load("Cars/GreenCar"), _carParent);

                    _textName.text = "Green car";
                    _textExplain.text = "초록차 입니다.";

                    break;
                }
            case ECarType.Hiace:
                {
                    _gbjCar = Instantiate((GameObject)Resources.Load("Cars/Hiace"), _carParent);
                    _carParent.transform.localScale = new Vector3(35f, 35f, 35f);

                    _textName.text = "HI Ace";
                    _textExplain.text = "하이에이스 입니다.";

                    break;
                }
            case ECarType.RedBus:
                {
                    _gbjCar = Instantiate((GameObject)Resources.Load("Cars/RedBus"), _carParent);
                    _carParent.transform.localScale = new Vector3(30f, 30f, 30f);

                    _textName.text = "Red Bus";
                    _textExplain.text = "빨간 버스 입니다.";

                    break;
                }
            case ECarType.BlueTruck:
                {
                    _gbjCar = Instantiate((GameObject)Resources.Load("Cars/BlueTruck"), _carParent);
                    _carParent.transform.localScale = new Vector3(25f, 25f, 25f);

                    _textName.text = "Blue Truck";
                    _textExplain.text = "파란 트럭 입니다.";

                    break;
                }
            case ECarType.TukTuk:
                {
                    _gbjCar = Instantiate((GameObject)Resources.Load("Cars/TukTuk"), _carParent);

                    _textName.text = "TUK TUK";
                    _textExplain.text = "툭툭 입니다.";

                    break;
                }
            case ECarType.YellowBus:
                {
                    _gbjCar = Instantiate((GameObject)Resources.Load("Cars/YellowBus"), _carParent);
                    _carParent.transform.localScale = new Vector3(30f, 30f, 30f);

                    _textName.text = "Blue bus";
                    _textExplain.text = "파란 버스 입니다.";

                    break;
                }
        }
    }

    public void Onbtn()
    {
        // 의존성이 없음.
        _onBtn?.Invoke(_carType);
    }
}

 

SetSlot() 함수에서 switch-case 문으로 슬롯을 세팅해주고, 슬롯(버튼)을 클릭하면 ECarType 정보를 넘겨주기 위해서 

Action<ECarType> _onBtn에 매개변수 onBtn을 대입해주었다.

 

슬롯을 클릭하면 Panel_SelectCarInit()에서 슬롯을 세팅과 초기화를 해주는 함수이다.

Init함수는 Panel_Main에서 Select버튼을 클릭했을 때 슬롯을 세팅해주고,

Panel_SelectCar에서 슬롯을 클릭하면 Panel_MainChangeCar() 함수를 실행하며 콘솔에 로그를 출력한다.

 

 

 


* 공부하는 단계입니다. 잘못된 부분이 있다면 피드백 부탁드립니다😊

* e-mail : heehee970@naver.com