Event Bus Pattern 이란?

특정 이벤트가 발생하였을 때 이벤트가 발생했다는 사실을 외부 클래스에게 알려주는 방법에는 어떤 것이 있을까요?

외부 클래스는 해당 이벤트가 발생했다는 것을 알아차리기 위해 항시 대기를 하고 있어야 하는걸까요?

항시 대기라고 한다면 Unity Update()메소드를 이용하여 매 프레임마다
원하는 값을 받아오고 상태가 바뀌었는지 확인하는 방법이 있을 것 같은데요.

좋은 방법이라고 생각이 되지는 않습니다.

이러한 상황에 이벤트 버스 패턴을 활용할 수 있습니다.

이벤트 버스 패턴은 발행/구독 패턴의 형식을 띄는 패턴입니다.


Event Bus Pattern의 구성

Event Bus Pattern Diagram

  1. Publisher : 이벤트 게시자

  2. Subscriber : 이벤트 구독자로, 이벤트가 발생하면 Bus를 통해 알림을 받습니다.

  3. Bus : 구독자에게 이벤트를 등록해주고, 이벤트가 발생할 경우 Publisher에게서 신호를 받아 Subscriber에게 알려주는 역할을 합니다.


Event Bus Pattern의 구현

[EventType]

1
2
3
public enum EventType{
COUNTDOWN, START, PAUSE, QUIT
}

[EventBus]

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
32
33
34
35
36
37
38
39
40
41
42
public class EventBus{
// 이벤트 종류와 구독자 간 관계를 목록으로 관리
private static readonly IDictionary<EventType eventType, UnityAction listener> Events = new Dictionary<EventType, Unity Event>();

//구독자 등록
public static void Subscribe(EventType evenetType, UnityAction listener)
{
UnityEvent thisEvent;

if(Events.TryGetValue(evenetType, out thisEvent))
{
thisEvent.AddListener(listener);
}
else
{
thisEvent = new UnityEvent();
thisEvent.AddListener(listener);
Events.Add(evenetType, thisEvent);
}
}

//구독 해제
public static void Unsubscribe(EventType type, UnityAction listener) {
UnityEvent thisEvent;

if(Events.TryGetValue(type, out thisEvent))
{
thisEvent.RemoveListener(listener);
}
}

//이벤트
public static void Publish(EventType type)
{
UnityEvent thisEvent;

if(Events.TryGetValue(type, out thisEvent))
{
thisEvent.Invoke();
}
}
}

[StartCount] : 구독자이자 발행자 역할을 하는 클래스

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
32
33
34
35
36
37
public class StartCount : MonoBehaviour
{
private float _currentTime;
private float _duration = 3.0f;

private void OnEnable()
{
EventBus.Subscribe(EventType.COUNTDOWN, StartTimer);
}
private void OnDisable()
{
EventBus.Unsubscribe(EventType.COUNTDOWN, StartTimer);
}
private void StartTimer()
{
StartCoroutine(Countdown());
}

private IEnumerator Countdown()
{
_currentTime = _duration;

while(_currentTime > 0)
{
yield return new WaitForSeconds(1f);
_currentTime--;
}

EventBus.Publish(EventType.START);
}

private void OnGUI()
{
GUI.color = Color.blue;
GUI.Label(new Rect(125, 0, 100, 20), "COUNTDOWN : ", _currentTime.ToString());
}
}

오브젝트가 활성화 되었을 경우 event 구독을 시작하고 비활성화되었을 경우 더이상 이벤트 알림을 받을 필요가 없으므로 구독 취소를 해줍니다.

위의 클래스는 활성화되었을 경우 COUTNDOWN 이벤트를 구독하고

Countdown이 실행완료되면 START 이벤트를 발행합니다.

게임시작 버튼을 눌러 COUNTDOWN이벤트가 발행된다면 위의 작업이 실행됩니다.


Event Bus Pattern의 장단점

장점

이벤트 버스는 게시자와 구독자 간 느슨한 결합을 유지시켜줍니다.

오브젝트들이 서로 직접 참조하지 않고 이벤트를 통신할 수 있습니다.

이는 굳이 게시자와 구독자가 누구인지 알아둘 필요가 없다는 것을 뜻하기도 합니다.

단점

전역적으로 접근하기 때문에 디버깅과 유닛 테스트를 어렵게 함으로 프로젝트 관리 또한 어렵게 만든다.


Event Bus 사용 시기

  1. 빠른 프로토타이핑 : 분리 상태를 유지하면서 이벤트로 각자 다른 동작을 하는 컴포넌트를 쉽게 생성할 수 있었습니다. 빠르고 쉽게 프로토타이핑하고 싶을 때 사용하기 좋습니다.

  2. 프로덕션 코드 : 게임 이벤트를 더 정교하게 관리하지 않아도 되는 프로덕션 코드에서 사용할 수 있습니다.

  3. 복잡한 이벤트 타입이나 구조체를 다루지 않아도 될 경우 이벤트 버스 패턴의 이점이 보입니다.


Event Bus 패턴과 Observer 패턴

이벤트 버스 패턴과 옵저버 패턴은 비슷한 성향을 지니고 있지만 명백하게 차이점이 있습니다.

이는 Event bus의 여부로 볼 수 있습니다.

옵저버 패턴의 경우 Subject(구독자)가 옵저버를 등록하고 직접 옵저버에 알려주어야 하지만,

이벤트 버스 패턴의 경우 Event Bus에 메시지를 전달하기만 하면 구독자와 게시자의 존재를 파악할 필요가 없다는 점입니다.


Event Bus 패턴의 대안

  1. 옵저버 : 서브젝트가 옵저버 목록을 유지 관리하고 내부 상태 변경을 알리는 패턴입니다.

  2. 이벤트 큐 : 게시자가 생성한 이벤트를 큐에 저장하고 편한 시간에 구독자에게 전달하는 패턴입니다.

[참조]

[C#] Garbage Collector 란?

Garbage Collector란 가비지 컬렉터라고 하며 GC라고 부르기도 합니다.

GC는 CLR(공용 언어 런타임)에서 제공하는 자동 메모리 관리 소프트웨어입니다.

C#으로 작성한 소스 코드를 컴파일해서 실행 파일을 만들고 실행하면,

CLR은 프로그램을 위해 메모리를 확보합니다.

메모리의 공간을 확보해서 관리되는 힙 메모리를 마련합니다.

힙 메모리의 첫 번째 주소에 다음 객체를 할당할 메모리의 포인터를 위치시킵니다.

각 프로세스는 가상주소공간을 포함합니다.

이 때, 가상 주소 공간은 조각화될 수 있는데

가비지 컬렉터는 객체를 할당할 수 있는 블록을 찾게 됩니다.


GC가 메모리를 관리하는 방법

데이터가 할당된 메모리의 위치를 참조하는 객체를 루트라고 합니다.

JIT 컴파일러는 이 루트들을 그래프로 만들고 CLR은 루트 목록을 관리하며 상태를 갱신하다.

애플리케이션 루트에는 정적 필드, 스레드 스택의 지역 변수, CPU 레지스터, GC핸들, finalize 큐가 포함됩니다.

GC는 루트목록을 참조하며 힙을 차지하고 있는 쓰레기(힙 메모리를 차지하고 있으나 이를 참조하는 데이터가 없는 개체)들을 수집합니다.


GC 동작 순서

  1. 작업을 진행하기 전, GC는 모든 객체가 쓰레기라고 가정합니다.
루트 목록은 그래프로 관리를 하니, 그래프에 없는 개체라고 생각하시면 됩니다.

  1. 루트 목록 내 어떤 루트도 메모리를 가리키지 않는다고 가정합니다.

  2. 루트 목록을 순회하면서 각 루트가 참조하고 있는 힙 개체와의 관계 여부를 조사합니다.

  3. 어떤 루트와도 관계가 없는 힙의 개체들은 쓰레기로 간주합니다.

  4. 쓰레기 개체가 가지고 있던 메모리는 이제 비어 있는 공간이 됩니다.

  5. 비어 있는 공간에 개체를 이동시켜 채워넣습니다.


GC 메모리 정리 순서

  1. 가비지 컬렉터는 메모리를 할당하고 있는 개체들을 3개의 세대로 나눕니다. (0세대, 1세대, 2세대)

  2. 0세대는 나이가 적은 개체, 2세대는 나이가 많은 데이터를
    위치시킵니다.

나이는 가비지 컬렉션을 겪은 횟수로 정해집니다.

  1. 가비지 컬렉터는 0세대를 위주로 개체 회수를 진행합니다.

0세대에서 개체가 회수되지 않았다면 다음 세대로 넘어가게 됩니다.


GC 호출 시기

  1. 엔진은 수집을 수행하기에 가장 적합한 시간을 결정하고, 시간에 맞춰 주기적으로 GC를 호출합니다.

  2. 시스템의 실제 메모리가 부족한 경우 호출합니다.

  3. 할당된 개체에 사용되는 힙 메모리가 허용하는 임계값을 초과한 경우 호출합니다. (3개의 세대 중 한 세대의 잔존율이 높을 경우 해당 세대의 할당 임계값을 늘립니다.)

  4. GC.Collect 메소드를 호출하여 호출하기도 하지만 GC는 주기적으로 호출되므로 수동으로 호출할 필요는 없습니다.

GC 수행 작업

  1. 모든 활성 개체를 찾아 루트 목록을 만드는 표시 작업

  2. 압축될 개체에 대한 참조를 업데이트 하는 재배치 작업

  3. 비활성 개체에 의해 점유된 공간을 회수하고 남은 개체를 압축하는 압축작업

Singleton Pattern 이란?

싱글톤 패턴이란 객체의 인스턴스가 오직 1개만 생성되는 패턴입니다.

즉, 런타임 동안 메모리에 오직 하나의 인스턴스만 존재하는 것을 의미합니다.

싱글톤 패턴의 주요 목적은 유일성을 보장하는 것입니다.

그러므로 일관되고 유일하며 전역적으로 접근할 수 있는 시스템을 관리하는 클래스에 사용할 경우 도움됩니다.


Singleton Pattern의 구성

Singleton Pattern

  1. static 인스턴스를 생성하여 메모리에 할당해 둡니다.
  2. 생성되어 있는 인스턴스를 Client들이 접근합니다.
  3. 이미 인스턴스가 생성되어 있는 경우 같은 유형의 인스턴스가 발견되면 삭제합니다.

Singleton 구현

[Singleton Class]

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
32
33
34
35
36
37
38
39
40
using UnityEngine;

public class Singleton <T> : MonoBehaviour where T : Component
{
private static T instance;

public static T Instance
{
get
{
if(instance == null)
{
//생성된 오브젝트를 순차적으로 확인하기때문에 주의
instance = FindObjectOfType<T>();
if(instance == null)
{
GameObject obj = new GameObject();
obj.name = typeof(T).Name;
instance = obj.AddComponent<T>();
}
}
return instance;
}
}

//가상 함수로 지정한 경우 파생 클래스에서 재정의가 가능
public virtual void Awake()
{
if(instance == null)
{
instance = this as T;
//static 메서드로 씬 전환이 발생하여도 파괴되지 않는다.
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
}

[GameManager Class]

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
32
33
34
35
36
37
38
using System;
using UnityEngine;
using UnityEngine.SceneManagement;

public class GameManager : Singleton<GameManager>
{
private DateTime _sessionStartTime;
private DateTime _sessionEndTime;

/// <summary>
/// Start Method
/// TODO:
/// - 플레이어 세이브 로드
/// - 세이브가 없다면 플레이어를 등록 씬으로 리다이렉션
/// - 백엔드 호출하여 일일 챌린지와 보상 획득
/// </summary>
void Start()
{
_sessionStartTime = DateTime.Now;
Debug.Log($"Game session start: {DateTime.Now}");
}

private void OnApplicationQuit()
{
_sessionEndTime = DateTime.Now;
TimeSpan timeDifference = _sessionEndTime.Subtract(_sessionStartTime);
Debug.Log($"Game session ended: {DateTime.Now}");
Debug.Log($"Game session lasted: {timeDifference}");
}

private void OnGUI()
{
if(GUILayout.Button("Next Scene"))
{
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex + 1);
}
}
}

Singleton Pattern의 장단점

장점

  1. 시스템의 전역 접근점을 만들 수 있습니다.
  2. 메모리 자원 낭비를 방지할 수 있습니다.

단점

  1. 클래스 간의 의존성이 높아지게 됩니다.
  2. 접근하는 객체들을 추적하기가 어려워집니다.
  3. 결합도가 높아지므로 유닛 테스트가 힘들어집니다.

State Pattern 이란?

유한한 상태를 관리할 때 사용하는 패턴입니다.
구현해야하는 상태가 많을 수록 불안정해지는 경우가 많습니다.

잘못된 플로우로 흘러가는 경우 원하는 상태로 도달하지 못하는 상황도 발생합니다.

또 상태를 구현하다보면 다양한 코드가 작성되어 어떤 상태인지 정확하게 파악하기 힘들어지기도 합니다.

State Pattern을 이용하여 상태를 캡슐화하고 클래스를 간소화하여 유지관리를 용이하게 할 수 있습니다.


State Pattern의 구성

State Pattern Diagram

  1. Context 클래스는 클라이언트가 객체의 내부 상태를 변경할 수 있도록 요청하는 인터페이스인 IState를 정의합니다.

  2. Context 클래스는 현재 상태에 대한 정보를 가집니다.

  3. IState인터페이스는 상태 클래스인 ConcreteState 클래스가 상속받아 구현합니다.

  4. 클라이언트는 객체의 상태를 업데이트할 때 Context 객체를 활용하여 원하는 상태로 설정할 것을 요청합니다.


State Pattern의 구현

State Pattern을 구현하기에 앞서 상태들에 대한 정의가 필요합니다.

저는 캐릭터의 상태로 정지, 걷기, 점프 3가지의 상태를 생각해두었습니다.

[IPlayerState 인터페이스] : IState

1
2
3
4
public interface IPlayerState
{
void Handle(PlayerController controller);
}

[PlayerController 클래스] : Context에 상태 전달

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
32
33
34
35
36
37
38
39
40
41
public class PlayerController : MonoBehaviour
{
public float maxSpeed = 2f;
public float jumpPower = 2f;

public float CurrentSpeed { get; set; }
public Vector3 CurrentDirection { get; set; }
public bool CurrentJump { get; set; }

//현재 상태 정보를 위해 각 상태 별로 생성
private IPlayerState _walkState, _stopState, _jumpState;
private PlayerStateContext _playerStateContext;

private void Start()
{
_playerStateContext = new PlayerStateContext(this);

//각 상태들을 정의
_walkState = gameObject.AddComponent<PlayerWalkState>();
_stopState = gameObject.AddComponent<PlayerStopState>();
_jumpState = gameObject.AddComponent<PlayerJumpState>();

_playerStateContext.Transition(_stopState);
}

#region 상태 전이 메서드
public void WalkPlayer(Vector3 direction)
{
CurrentDirection = direction;
_playerStateContext.Transition(_walkState);
}
public void StopPlayer()
{
_playerStateContext.Transition(_stopState);
}
public void JumpPlayer()
{
_playerStateContext.Transition(_jumpState);
}
#endregion
}

[PlayerStateContext 클래스] : Context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class PlayerStateContext
{
public IPlayerState CurrentState
{
get; set;
}

private readonly PlayerController _playerController;

public PlayerStateContext(PlayerController playerController)
{
_playerController = playerController;
}
public void Transition()
{
CurrentState.Handle(_playerController);
}
public void Transition(IPlayerState state)
{
CurrentState = state;
CurrentState.Handle(_playerController);
}
}

[PlayerWalkState 클래스] : ConcreteState

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
public class PlayerWalkState : MonoBehaviour, IPlayerState
{
private PlayerController _playerController;

public void Handle(PlayerController playerController)
{
if (!_playerController)
{
_playerController = playerController;
}
_playerController.CurrentSpeed = _playerController.maxSpeed;
}

void Update()
{
if (_playerController)
{
if (_playerController.CurrentSpeed > 0)
{
_playerController.transform.Translate(
Time.deltaTime * _playerController.CurrentSpeed * _playerController.CurrentDirection);
}
}
}
}

[ClientState 클래스]

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
32
33
34
using UnityEngine;

public class ClientState : MonoBehaviour
{
private PlayerController _playerController;
private float _inputHorizontal;
private float _inputVertical;
private Vector3 _direction;

private void Start()
{
_playerController = GetComponent<PlayerController>();
}

private void Update()
{
_inputHorizontal = Input.GetAxis("Horizontal");
_inputVertical = Input.GetAxis("Vertical");

_direction = new Vector3(_inputHorizontal, 0, _inputVertical);

//조건에 따라 상태 전이 메서드 호출
if(_direction == Vector3.zero){
_playerController.StopPlayer();
}
if(_direction != Vector3.zero) {
_playerController.WalkPlayer(_direction);
}
if(Input.GetKeyDown(KeyCode.Space)) {
_playerController.JumpPlayer();
}
}
}

WalkState를 제외하고 Stop, Jump 도 작성을 해두었지만 WalkState 클래스만 예시로 보여드렸습니다.

IPlayerState 인터페이스를 생성하고

WalkState는 IPlayerState를 상속받아 Handle메서드를 구현해두었습니다.

PlayerController는 클라이언트가 요청한 상태 값을 PlayerStateContext에게 전달,
PlayerStateContext는 현재 상태 정보를 보관하도록 작성해두었습니다.

이렇게 작성해두니 상태별로 관리하기가 수월해진 것이 느껴집니다.

상태를 수정하려거든 각 상태 클래스 또는 상태를 호출하는 ClientState 클래스의 상태 호출 조건을 수정하면 됩니다.

상태를 추가하려거든 특정 상태 클래스를 추가, PlayerController클래스에서 전이 메서드 추가,
전이 메서드 호출하는 코드 추가를 하면 됩니다.

이렇게 정리해보니 새로운 상태를 추가하거나, 기존 상태에 대한 변경점이 발생한다면 어느 부분을 추가/수정해야하는지 명확해졌습니다.


State Pattern의 장단점

장점

  1. 상태별로 클래스 관리가 가능합니다.
  2. 유지관리가 수월해집니다.

단점

  1. 추가할 때마다 상태 클래스를 추가해야하므로 관리해야하는 클래스의 수가 증가합니다.
  2. 상태 전환이 빈번하게 발생한다면 확인 조건이 늘어나므로 코드가 방대해지고 수행 시간이 길어지게 됩니다.

유니티를 이용하여 상태를 구현하다보면 상태 변경으로 인해 코드가 늘어나고 관리하기 힘들어질 때가 있습니다.

유니티 내에서는 애니메이터 상태에 추상 클래스인 StateMachineBehaviour 클래스를 상속받은 상태 정의 클래스를 컴포넌트로 추가하여 활용할 수 있습니다.


[참조]

[Unity] Object Pooling 개요

게임 속 플레이어가 몬스터를 처치하기 위해 총알을 발사한다고 한다면 총알을 생성하고 총알을 삭제하는 작업이 반복될 것입니다.

프레임 속도를 유지하면서 CPU에 부담을 주지 않으려면 빈번하게 파괴되고 생성되는 요소들을 메모리에 할당해두고 있는 것이 좋습니다.

오브젝트를 미리 생성해두었다가 필요할 때에 사용하고 다시 되돌려놓는 방법을 Object Pooling 패턴이라고 합니다.


Object Pooling의 작동원리

  1. 컨테이너 형식의 풀은 초기화된 오브젝트 목록을 메모리에 남겨둡니다.

  2. 사용자는 사용할 오브젝트 인스턴스를 Pool에 요청할 수 있습니다.

  3. 만약 주어진 시간 내에 풀 내의 인스턴스가 충분하지 않다면 새로운 인스턴스가 동적으로 생성됩니다.

  4. 풀을 빠져나간 객체 중 클라이언트에서 더 이상 사용되지 않게되면 풀로 돌아가게 됩니다.

  5. 이 때, 풀에 공간이 없다면 돌아오려는 객체의 인스턴스를 파괴합니다.


장단점

Object Pooling 패턴에 대해서는 여러 개발자마다 입장이 다릅니다.

패턴을 사용할 경우 장단점은 어떤 것이 있을까요?

장점

  1. 메모리 사용률이 예측 가능합니다. 풀을 생성할 때 풀 안에 생성될 오브젝트의 갯수를 미리 지정해두기 때문에 메모리를 어느만큼 사용할지 예측이 가능합니다.

  2. 오브젝트를 Instantiate, Destroy하는 과정에서 가비지 콜렉터를 호출하게 되면서 프레임 드랍이 발생하는데 해당 작업의 빈도가 줄어들게 됩니다.

단점

  1. 메모리를 사용하지 않는 상황에도, 기본적으로 메모리의 공간을 할당하고 있습니다.

  2. 초기화의 번거로움이 존재합니다. 오브젝트 풀링을 사용할 경우 초기화 코드를 작성해주어야 합니다.


장점만 가지고 있는 패턴은 없는 것 같습니다. 그러므로 상황에 따라 신중하게 패턴을 사용할지 말지 결정해야합니다.

최적화가 필요한 시점에 어떤 부분에 적용을 시키는 것이 효율이 좋을지 검토해보는 것이 좋을 것 같습니다.

[C#] Parameter 란?

Parameter(매개 변수)란 함수를 호출한 곳에서 인수를 전달받아 인수의 값을 함수의 내부에서 사용할 수 있도록 도와주는 변수를 뜻합니다.

1
2
3
4
5
6
7
8
9
10
11
static void Main(string[] args){
int _num1 = 3;
int _num2 = 6;
int _sum = Method(_num1,_num2); //인수 전달

}

static public int Method(int num1, int num2) //매개 변수
{
return num1 + num2;
}

위와 같이 Method를 호출하는 Main함수에서 _num1, _num2를 인수(Argument)로 넘겨주고

호출된 Method에서 인수 값을 담고 있는 파라미터인 num1, num2를 이용하여 결과 값을 전달해주고 있습니다.

인수란 함수를 호출할 때 넘겨주는 변수 값입니다.


값 형식 전달, 참조 형식 전달

  1. 값으로 전달 : 변수의 복사본을 전달
  2. 참조로 전달 : 변수에 대한 주소를 전달
형식 값으로 전달 참조로 전달
값 형식 호출자에게 변경 내용이 반영되지 않음 호출자에게 변경 내용이 반영됨
참조 형식 호출자에게 변경 내용이 반영됨 호출자에게 변경 내용이 반영됨

Parameter 키워드

개발자는 필요함에 따라 파라미터 키워드를 사용할 수 있습니다.

그럼 파라미터 키워드에는 어떤 것들이 있을까요?


ref

일반적으로 c#의 인수는 값이 복사되어 호출된 메소드에게 전달됩니다.

이런 경우 메서드 내에서 매개변수의 값이 수정되어도 호출된 곳에서 변수의 값은 변하지 않습니다.

메소드에서 변동된 값이 호출한 곳에서도 적용이 되었으면 좋겠다면
ref 키워드를 사용하면 됩니다.

ref 키워드는 reference로 참조에 의한 인수 전달 키워드입니다.

1
2
3
public int Method(ref int num1, int num2){
return num1 + num2;
}

ref 키워드를 사용하기 위해
메소드의 매개변수 앞과, 메소드를 호출할 때 인수 앞에 ref키워드를 명시해주어야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void Main(string[] args){
int _num1 = 3;
int _num2 = 4;
Method(ref _num1,_num2);
Console.WriteLine($"Main Start!");
Console.WriteLine($"_num1: {_num1} , _num2: {_num2}");
}

static public void Method(ref int num1, int num2){
num1 = num1 + 1;
num2 = num2 + 1;

Console.WriteLine($"Method Start!");
Console.WriteLine($"num1: {num1} , num2: {num2}");
}

[결과]

1
2
3
4
Method Start!
num1: 4 , num2: 5
Main Start!
_num1: 4 , _num2: 4

결과를 보면 메소드 내에서 num2는 증가되어 5를 가지고 있지만
main 에서 _num2는 증가되지 않는 4의 값을 가지고 있습니다.

num2와 다르게 _num1은 메소드 내에서 증가한 것이
main에서도 똑같이 증가된 것을 확인할 수 있습니다.


out

out은 ref와 비슷하게 참조에 의한 매개변수을 이용할 수 있게 도와주는 키워드입니다.

사용방식 또한 ref와 같습니다.

그럼 둘의 다른점은 무엇일까요?

ref일 경우 인수를 초기화해야하지만,

초기화가 되었기때문에 호출된 메소드 내에서 새로운 값을 반드시 할당하지 않아도 됩니다.

out일 경우 인수가 할당되지 않아도 되지만,

호출된 메소드 내에서 새로운 값을 반드시 할당해주어야 합니다.


자세한 내용은 Microsoft c# 가이드 를 참고하시면 됩니다.

[C#] Generic 이란?

제너릭이란 데이터의 타입을 일반화하는 것을 의미합니다.

1
2
3
4
public void Method<T> (T parameter1, T parameter2)
{
---내용
}

원래라면 자료형에 관계없이 동일한 작업을 진행해야하는 메소드가 필요할 경우 오버라이딩을 통해 리턴 형식이 다르거나, 다른 종류와 개수의 파라미터를 가지는 메소드를 정의해주어야 했습니다.

1
2
3
4
5
6
7
8
9
10
11
public void Print(int num){
Console.WriteLine($"{num}");
}

public void Print(string str){
Console.WriteLine($"{str}");
}

public void Print(float num){
Console.WriteLine($"{num}");
}

같은 메소드를 여러번 정의하는 것은 여간 번거로운 일이 아닙니다.

그렇기 때문에 어떤 형식이든 이용할 수 있도록 제너릭으로 일반화 메소드를 정의해주면 간편해집니다.

1
2
3
public void Print<T> (T Parameter){
Console.WriteLine($"{Parameter}");
}

제너릭은 코드를 재사용할 때, 형식의 안전성과 성능을 최대화합니다.

제너릭은 제너릭 클래스로 가장 많이 활용됩니다.

각 인스턴스에서 클래스에 있는 모든 T는 컴파일할 때 제너릭 형식을 의미하는 메타데이터가 생성됩니다.

그 후 런타임에 메타데이터를 확인하고 특수화된 제너릭 클래스를 생성합니다.

이 때 사용된 형식이 값 형식인지 참조 형식인지에 따라 제너릭 클래스를 생성하는 방식이 달라집니다.


값 형식일 경우

만약 정수를 사용한다면 매개 변수를 정수로 적절히 대체하여 특수화된 버전의 클래스를 생성합니다.

프로그램 코드에서 해당 클래스를 정수를 이용하여 다시 사용한다면

이전에 생성했던 특수화 클래스를 다시 사용합니다.

하지만 다른 값 형식이 들어온다면,

다른 버전을 생성하여 적절한 위치에 다른 값 형식을 대체합니다.

참조 형식일 경우

런타임에서 MSIL의 매개 변수를 개체 참조로 대체하여 특수화된 제너릭 형식을 만듭니다.

이후 참조 형식과 관계없이 생성된 형식이 인스턴스화될 때마다
런타임에서 이전에 만든 특수화된 버전의 제너릭 형식을 재사용합니다.

재사용할 때는 이미 생성된 클래스의 인스턴스를 생성하여 변수가 인스턴스를 참조하게 됩니다.

자료구조란?

자료구조란 데이터의 저장 방법 또는 데이터 관련 연산의 총집합을 의미합니다.

자료구조는 단순 자료구조, 복합 자료구조, 파일 구조로 나누어 볼 수 있습니다.

DataStructure

단순 자료구조

단순 자료구조란 기본적으로 제공하는 정수, 실수, 문자를 포함합니다.

복합 자료구조

복합 자료구조는 선형구조, 비선형구조로 구분할 수 있습니다.

선형구조

선형구조란 데이터 요소를 순차적으로 연결하는 자료구조입니다.

  1. 배열
  2. 링크드 리스트
  3. 스택

비선형구조

비선형구조란 데이터 요소가 비순차적, 트리 형태로 연결되어있습니다.

  1. 트리
  2. 그래프

파일구조

파일 구조는 파일을 저장하는 구조입니다.

  1. 순차 파일구조
  2. 색인 파일구조
  3. 직접 파일구조

Comment

Quaternion이란?

안녕하세요. 이번 포스트에서 Quaternion에 대한 내용을 정리해보려고 합니다.

Unity에서는 오브젝트의 회전 값을 쿼터니언으로 표현하고 있습니다.

상세한 내용은 Unity API 문서를 참고하시길 바랍니다.

쿼터니언을 사용하는 이유는 Euler의 단점을 개선하기 위해서인데요.

Euler는 무엇일까요?


Euler(오일러)

Euler란 3차원 공간의 절대 좌표를 기준으로 물체의 회전을 측정하는 방식입니다.

회전이 일어날 경우 세 축으로 나누어서 계산이 이루어집니다.

180도가 넘는 회전을 표현할 수 있습니다.

그 과정에서 짐벌락이라는 문제점이 발생할 수 있습니다.

짐벌락

짐벌락이란 3개의 축이 순차적으로 회전하는 도중 2개의 축이 겹치는 현상을 뜻합니다.

gimbal loack
참조 https://www.researchgate.net/figure/Gimbal-lock-When-the-pitch-Y-rotates-90-degrees-the-roll-X-and-yaw-Z-axes-become_fig14_46720588

오일러는 x, y, z 축을 기준으로 순차적 회전이 이루어집니다.

위의 그림에서는 x축 기준으로 회전이 발생하더니 z축과 겹쳐진 것을 확인할 수 있습니다.

이렇게 두개의 축이 겹쳐지면 y축이 회전할 경우 z축 기준 회전 값이 달라지게 됩니다.

Quaternion

Quaternion은 이러한 오일러의 짐벌락 현상을 해결하기 위해 사용됩니다.

Unity에서 오브젝트의 회전 상태를 나타내는 방식이며 (x, y, z, w)로 표현이 가능합니다.

각각의 인수는 특정 벡터 (x, y, z)와 벡터의 회전값인 스칼라(w)의 정보를 담고 있습니다.

Quaternion을 이용한 회전은 다른 축과 관계되어 측정되므로 180도 이상의 회전은 표현이 불가능합니다.


Quaternion 관련 메소드

앞에서 Quaternion의 개념에 대해 알아보았으니,

Quaternion을 어떻게 활용할 수 있는지 그와 관련된 메소드들을 알아보도록 하겠습니다.

Quaternion.identity

Quaternion.identity는

오브젝트가 최상위 오브젝트일 경우 월드 좌표 기준 회전이 발생하지 않은 기본 값,

오브젝트가 자식 오브젝트일 경우 부모 좌표 기준 회전이 발생하지 않는 기본 값을 담고 있습니다.

1
transform.position = Quaternion.identity;

Quaternion.eulerAngles

정의: public Vector3 eulerAngles

eulerAngles는 쿼터니언을 오일러로 나타내줍니다.

원래 오브젝트의 rotation 값은 Quaternion으로 정의 되어있기 때문에 오일러각을 이용하여 값을 수정하는 것이 불가합니다.

하지만 Quaternion.eulerAngles를 통해 오일러 값을 이용하여 회전 시키는 것이 가능합니다.

1
2
3
4
5
6
7
8
9
Vector3 _curEulerAngles;
Quaternion _curRotation;

void Start(){
//회전하려는 값
_curEulerAngles = new Vector3(60, 0, 0);
//eulerAngles메소드를 통해 오브젝트의 오일러값에 접근해준다.
transform.rotation.eulerAngles = _curEulerAngles;
}

Quaternion.Euler

정의: public static Quaternion Euler(float x, float y, float z)

오일러 회전 값을 받아 쿼터니언 회전 값으로 리턴해줍니다.

1
2
3
4
5
private Vector3 _rotVector;
void Start(){
_rotVector = new Vector3(0,50,00;)
transform.rotation = Quaternion.Euler(_rotVector);
}

Quaternion.FromToRotaion

정의: public static Quaternion FromToRotation(Vector3 fromDirection, Vector3 toDirection)

fromDirection에 입력된 벡터를 하나의 축으로 삼아 toDirection의 벡터 축으로 회전시켜줍니다.

1
2
3
private void Start(){
transform.rotation = Quaternion.FromToRotation(Verctor3.forward, Vector3.up);
}

Quaternion.LookRotaion

정의: public static Quaternion LookRotation(Vector3 forward, Vector3 upwards = Vector3.up)

forward는 바라보고 싶은 방향이고, upwards는 오브젝트의 머리가 향하는 방향입니다.

1
2
3
4
5
6
private Transform = _otherObject;
private Vector3 _relativeDir;
private void Update(){
_relativeDir = _otherObject.position - transform.position;
transform.rotation = Quaternion.LookRotation(_relativeDir, Vector3.up);
}

위와 같이 작성하게 되면 _relativeDir는 상대오브젝트의 좌표에서 현 오브젝트의 좌표를 빼주었으므로

현 오브젝트 -> 상대 오브젝트 벡터를 뜻합니다.

해당 벡터 방향을 바라보면서 현 오브젝트의 머리는 위를 바라볼 수 있도록 작성해준 것입니다.

Quaternion.RotateTowards

정의: public static Quaternion RotateTowards(Quaternion from, Quaternion to, float maxDegreesDelta)

from 회전값을, to 회전값까지 maxDegressDelta 만큼 회전시킨 회전 결과값을 Quanternion으로 리턴해줍니다.

1
2
3
void Update(){
transform.rotation = Quaternion.RotateTowards(transfrom.rotation, _target.rotation, Time.deltaTime * rotSpeed);
}

이번 포스터는 여기까지 작성하고 마치겠습니다.