오늘은 C프로젝트에서 Shader를 이용하여 오브젝트가 물 background 위에 있을 때 잠겨있는 듯한 모습을 연출하려고 한다.

 

유니티에서 Shader를 만드는 방법은 크게 두 가지 방식이 있다.

첫 번째론 셰이더 언어를 이용하여 직접 코드를 작성하여 만드는 방식이고,

두 번째 방법으론 유니티에서 제공하는 비주얼 툴인 Shader Graph를 이용하여 만드는 방식이다.

나는 두 가지 방법 중 후자를 선택하여 만들어보려고 한다.

 

시작에 앞서 나는 유니티 프로젝트를 진행하면서 한 번도 셰이더를 적용하여 결과물을 만들어본 적이 없다...

그렇다보니 막히는 부분이 너무 많았다... ㅠ

그렇기 때문에 먼저 깊게까진 아니더라도 Rendering Pipeline에 대해서 공부를 해보았다.

그 이후 한 예제를 들어 셰이더를 만드는 방법에 대한 유튜브 영상도 참고해 보았다.

 

아래의 개발 과정은 셰이더에 대한 기본적인 지식이 있다는 가정하에 설명이 되므로

혹시 보는 과정 중 설명이 부족한 부분이 있다면 직접 찾아서 궁금증을 해결하자!

 

이제 본격적으로 직접 셰이더 그래프를 이용하여 만들어 볼 시간이다!

그러기 위해선 먼저 셰이더 그래프를 먼저 생성해 준다.

셰이더 그래프를 만드는 종류는 여러 가지가 있는데 이 중 현재 필요한 형태의 셰이더 그래프를 생성해 주면 된다.

기본적으로 이 프로젝트는 지금 당장은 아니지만 URP를 적용하여

Lighting에 대한 처리를 하기 위해 "Sprite Lit Shader Graph"로 생성했다.

 

우선 아이디어가 필요했다.

어떤 방식을 이용하여 물에 잠겨있는 듯한 연출을 표현할지...

처음 든 생각은 float형으로 depth 필드를 만든 이후 depth의 크기만큼 texture 일부분의 투명도는 0으로 해주었다.

우선 Texture의 특정 y값을 기준으로 아랫부분은 투명도를 0으로 해주기 위해 UV 좌표에서 y를 받아왔다.

적용할 머테리얼에서도 특정 y값을 조절할 수 있게 하기 위해

float형으로 _Depth라는 프로퍼티를 만들어 해당 값과 y를 빼주었다.

이후 step 노드를 통해 예쁘게 다듬어준다.

 

이렇게 나온 결과와 Main Texture의 알파값을 곱해준다면 특정 y값을 기준으로 아랫부분은 투명도가 0이 될 것이고 윗부분은 정상적으로 렌더링 되는 결과가 나올 것이다.

완성된 그래프를 저장한 이후 새로운 머테리얼을 만들어 방금 전에 만들어준 셰이더 그래프를 드래그 앤 드롭하여 적용시켜 준다.

만들어진 머테리얼을 오브젝트에 적용하여 결과를 확인하면 위와 같이 잘 적용되는 모습을 볼 수 있다.

하지만 물 위에 있는 듯한 느낌이 들긴 하지만 약간 어색함이 남아있다. (배경은 아직 없어 임시로 적용...)

 

아무래도 이 어색함은 잠겨있는 부분이 잔상처럼 보이지 않기 때문에 어색해 보이는 느낌이 드는 것 같아

texture의 일부 투명도를 0으로 줄였지만 조금 올려 뒤에 있는 물 배경과 형체가 자연스럽게 보이도록 수정해 보았다.

기존 그래프에서 일부를 추가하여 수정해 주었다.

우선 아랫부분과 윗부분을 나누어 아랫부분만 투명도를 조절할 수 있도록 만들어주었고

두 결과 값을 더하여 Main Texture의 알파값과 곱해주면 된다.

완성된 그래프를 저장한 뒤 다시 오브젝트를 확인해 보면 잘 적용된 모습을 볼 수 있다.

확실히 이전 방식보단 자연스러운 느낌이 남아있다.

지금 적용한 방식도 동적인 느낌이 안나다 보니 어색한 느낌이 여전히 남아있는 거 같다.

하지만 연출적인 부분에서 완전히 정해진 부분이 없어 정해지는 대로 더 나은 방식으로 자연스럽게 적용시켜보자 한다.

 

현재는 이 방식까지 밖에 생각 못했다 보니 혹시 다른 좋은 의견이 있으시다면 댓글 남겨주시면 감사하겠습니다!

지난번 C프로젝트 관련 게시글을 작성할 때 Scriptable Object를 이용하여 상태 패턴을 만드는 글을 작성해 보았었다.

https://lms0408.tistory.com/14

 

유니티 프로젝트 개발 일지 - 1 (상태 패턴)

오늘은 프로젝트 C에서 오브젝트의 상태 패턴을 구현해 볼 것이다. 이전 개인 프로젝트에서 상태 패턴을 구현할 땐,Player와 Montser 오브젝트가 있을 때 각기 다른 상태를 정의하고 같은 오브젝트

lms0408.tistory.com

오늘은 오브젝트의 상태가 있는 만큼 상태에 따른 애니메이션을 적용하고자 한다.

 

이 프로젝트에서는 오브젝트가 8방향을 가지고 있기 때문에 Animator에서 모든 클립들을 들고 와 상태 로직을 만들다 보면 지저분해 보일뿐더러 관리하기가 매우 힘들 것이다.

그렇기 때문에 오늘은 유니티에서 제공하는 블랜드 트리(Blend Tree)를 이용해 구현해보고자 한다.

 

단순히 애니메이션 클립에 관리가 힘든 부분이라면 Sub-State Machine을 만들어 해결할 수도 있다.

하지만 이러한 장점 이외에도 애니메이션을 매끄럽게 전환하는 데에도 이점이 있기 때문에

블랜드 트리를 이용하여 구현해보고자 한다.

 

유니티 Animator에 있는 블랜드 트리(Blend Tree)란?

다양한 애니메이션 클립을 하나의 상태로 묶어 특정 입력 값에 따라 혼합하여 재생하는 구조이다.

주로 3D 애니메이션을 표현할 때 이용하는 방식이지만

앞서 장점을 말했다시피 코드를 작성하지 않더라도 애니메이션을 매끄럽게 전환할 수 있다는 이점으로 인해 해당 방식을 이용하는 것이다.

 

블랜드 트리는 애니메이터에서 마우스 우클릭 시에 나오는 태그들 중 "Create State" 태그 내에서 새롭게 생성할 수 있다.

생성된 블랜드 트리를 더블 클릭하여 내부로 들어가면 기본 블랜드 트리가 만들어져 있을 것이다.

해당 블랜드 트리를 클릭하여 Inspector창을 확인해 보면 위와 같이 되어있다.

블랜드 트리에 이름은 자유롭게 수정이 가능하다.

Blend Type은 쉽게 말하자면 파라미터의 개수를 결정짓는다.

8방향을 표현하기 위해선 x, y 두 파라미터가 필요하므로 2D Simple Directional을 지정해 주면 된다.

 

이후 파라미터를 보면 두 개의 파라미터를 대입할 수 있고 기본값으로 Blend가 있을 것이다.

애니메이터에 float형 파라미터가 따로 생성되어 있지 않다면 블랜드 트리를 생성할 때 자동으로 기본 파라미터를 생성하여 적용되어 있게 된다.

 

먼저 x와 y의 값을 전달받기 위해 float형 파라미터 두 개를 생성하여 아래와 같이 지정해 주자.

이후 Motion 리스트를 추가해주면 새로운 모션을 추가할 것 인지,

새로운 블랜드 트리를 추가할 것 인지에 대한 선택이 나오며

여기선 "Add Motion Field"를 선택하여 새로운 모션을 추가해 주면 된다.

선택하여 만들어진 모션을 보면 재생할 애니메이션 클립을 넣을 수 있는 필드와

x, y의 값을 넣을 수 있는 필드에 값을 삽입할 수 있게 된다.

기본적으로 0을 기준으로 x가 양수일 땐 오른쪽, 음수일 땐 왼쪽

y가 양수일 땐 위쪽, 음수일 땐 아래쪽을 바라보는 애니메이션 클립을 재생시켜 줄 수 있도록 하고

대각선도 마찬가지로 위의 기준을 통해 값들을 삽입해 주면 완성이 된다.

모션을 추가할 때마다 파란색 포인트가 추가되는데 이 포인트는 모션의 방향을 나타내는 것이다.

그리고 파란색 포인트 외로 빨간색 포인트도 있는데

빨간색 포인트현재 파라미터 값에 따라 나타내고 있는 방향을 알려준다.

위의 사진의 경우는 현재 x와 y가 -1 값이 대입되어 왼쪽 아래의 모션을 나타내주고 있는 것이다.

 

이렇게 완성된 결과를 바탕으로 별도의 코드 없이 파라미터 값만을 수정하여

방향에 따른 애니메이션 전환이 잘 이루어지는 확인해 보면 된다.

이렇게 확인해보면 잘 적용된 것을 볼 수 있다.

적용해 보면서 제일 크게 느낀 장점은 별도의 조건과 연산 필요 없이

자동으로 자연스럽게 애니메이션을 전환해 주는 것이 정말 마음에 들었다.


참고 문서 : https://docs.unity3d.com/kr/560/Manual/class-BlendTree.html

현재 프로젝트 B에선 프로토타입을 위한 전투씬 부분의 개발을 진행하고 있는 상태이다.

전투는 다대다로 이루어지며 전투 인원이 정해져 있는 상태가 아니다.

 

전투를 진행하는 오브젝트들은 필수적으로 초기화를 위한 초기 정보가 필요하며,

해당 정보는 각 오브젝트에 적용하려 했으나 초기화 순서를 정형화하기 위해서이기도 하고

다른 이유들로 인해 한 곳에 모아둔 상태이다.

 

만약 각 오브젝트에 적용이 되어있다면 오브젝트를 생성한 이후 바로 값을 대입하고

필요없다면 오브젝트 자체를 삭제하면 되지만,

현재는 직렬화되어 있는 정보 리스트에 요소를 추가하거나 삭제를 한 이후

별도로 오브젝트들을 추가해주어야 하는 번거로움이 발생한 상태이다...

 

그래서 이번 기회에 커스텀 에디터를 통해 정보 리스트를 추가 삭제함에 따라 자동으로 오브젝트가 생성되고 삭제되는

에디터 코드를 작성해보고자 한다.

에디터 관련 코딩은 접해본 적이 없다보니 먼저 웹 검색을 통해 몇 가지를 알아보았다.

 

우선 Inspector에서 직렬화된 정보 리스트의 편집 여부를 어떻게 판단할 것 인가에 대해서 찾아보았고

SerializedProperty라는 클래스를 통해 직렬화된 정보를 받아올 수 있었다는 것을 알게 되었다.

[CustomEditor(typeof(BattleGameInitializer))]
public class CharacterCreater : Editor
{
    private SerializedProperty pInfoList;
    private SerializedProperty eInfoList;
    
    private void OnEnable()
    {
        pInfoList = serializedObject.FindProperty("playerInfos");
        eInfoList = serializedObject.FindProperty("enemyInfos");
    }
}

[CustomEditor(typeof(...)]의 선언은 특정 스크립트 타입의 Inspector를 커스터마이징 하기 위한 속성이다.

그다음 직렬화된 속성을 저장할 필드 변수를 선언해 주었다.

나는 Player와 Enemy의 정보 리스트를 따로 관리해주고 있기 때문에 두 개의 필드 변수를 선언해 주었다.

 

이후 OnEnable 메서드에서 스크립트에 정의된 필드 변수 이름으로 직렬화된 속성을 찾아서 저장해 주면 된다.

여기서 OnEnable은 MonoBehaviour에서 정의할 수 있는 OnEnable과 같은 것이다.

해당 메서드는 Inspector뷰에서 보일 때 호출된다.

 

SerializedProperty에는 다양한 프로퍼티를 통해 정보를 받아올 수 있다.

많은 프로퍼티중 나는 배열 크기만을 이용해 기존 리스트의 크기가 변경되었다는 조건식을 만들고,

리스트의 데이터가 추가되거나 삭제되었을 때 오브젝트의 인스턴스를 생성하고 삭제하는 메서드를 작성해 보자.

[CustomEditor(typeof(BattleGameInitializer))]
public class CharacterCreater : Editor
{
    private SerializedProperty pInfoList;
    private SerializedProperty eInfoList;
    
    private int prevPSize;
    private int prevESize;
    
    private GameObject playerCharPref;
    private GameObject enemyCharPref;
    
    private void OnEnable()
    {
        playerCharPref = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Prefabs/Character/PC.prefab");
        enemyCharPref = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Prefabs/Character/Enemy.prefab");

        pInfoList = serializedObject.FindProperty("playerInfos");
        eInfoList = serializedObject.FindProperty("enemyInfos");

        prevPSize = pInfoList.arraySize;
        prevESize = eInfoList.arraySize;
    }
    
    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();

        if (pInfoList.arraySize != prevPSize)
        {
            ModifyBattleCharInstance(pInfoList.arraySize > prevPSize);
            prevPSize = pInfoList.arraySize;
        }

        if (eInfoList.arraySize != prevESize)
        {
            ModifyBattleCharInstance(eInfoList.arraySize > prevESize, false);
            prevESize = eInfoList.arraySize;
        }
    }

    private void ModifyBattleCharInstance(bool set, bool isPlayer = true)
    {
        var _basedName = isPlayer ? "PC" : "Enemy";
        var _charParent = GameObject.Find($"{_basedName}List").transform;
    
        // 인스턴스 추가의 경우
        if (set)
        {
            Instantiate(isPlayer ? playerCharPref : enemyCharPref, _charParent).name = $"{_basedName}{_charParent.childCount}";
            return;
        }

        // 삭제의 경우
        Destroy(_charParent.GetChild(_charParent.childCount - 1).gameObject);
    }
}

우선 이전 배열의 크기를 저장할 수 있는 필드 변수들과

오브젝트 생성을 위해 필요한 프리팹을 저장할 필드 변수들을 선언해주었다.

 

해당 필드 변수들은 이전과 마찬가지로 OnEnable 메서드에서 대입해 주었다.

이전 배열 크기값은 현재 유지하고 있는 SerializedProperty의 현재 배열 크기를 대입해 주었고,

프리팹은 프리팹이 저장되어 있는 경로를 통해 반환받은 값을 대입해 주었다.

 

그리고 조건식에 만족했을 때 실행할 메서드를 정의해 주었다.

bool형 매개 변수 "set"은 데이터 추가인지 삭제인지를 나타내주고, "isPlayer"는 Player의 여부를 나타내준다.

인스턴스를 생성할 경우 특정 부모 Transform 하위에 생성되도록 지정해 주었고,

인스턴스를 삭제할 경우 부모 Transform에서 젤 하위에 있는 인스턴스를 삭제해 주도록 코드를 작성했다.

 

삭제를 할 땐 "Destroy"가 아닌 "DestroyImmediate"를 호출해주어야 한다.

Destroy의 경우 바로 삭제가 아닌 삭제 예약 즉, 한 프레임 이후 삭제를 시키는 메서드이고

DestroyImmediate바로 삭제를 해주는 메서드이다.

Editor 편집에서는 삭제를 예약해 주는 것이 아닌 바로 삭제를 해주어야 하기 때문에 DestroyImmediate를 호출해 주는 것이다.

 

조건식은 OnInspectorGUI 메서드에서 작성해 준다.

OnInspectorGUI는 Unity Editor에서 Insepctor 창이 새로고침 될 때 호출이 되고, 주로 GUI를 그려줄 때 정의하는 메서드이다.

 

조건식 역시 이전 코드들과 마찬가지로 간단하다.

이전 배열 사이즈와 현재 배열 사이즈를 비교하여 다르다면 정의해 준 메서드를 호출해 주면 의도한 대로 인스턴스가 생성되고 삭제될 것이다.

이렇게 잘 생성되고 삭제되는 것을 확인할 수 있다!

하지만 현재 오브젝트가 프리팹 오브젝트가 아닌 개별 오브젝트로 생성되기 때문에

현재 생성된 오브젝트들은 프리팹 수정에 영향을 받지 않는다.

그렇기 때문에 리팹 오브젝트로 생성하도록 코드를 수정해 주면 된다!

//Instantiate(isPlayer ? playerCharPref : enemyCharPref, _charParent).name = $"{_basedName}{_charParent.childCount}";
PrefabUtility.InstantiatePrefab(isPlayer ? playerCharPref : enemyCharPref, _charParent).name = $"{_basedName}{_charParent.childCount}";

수정 후 테스트 해보면 위와 같이 프리팹 오브젝트로 잘 생성되고 삭제되는 것을 알 수 있다.

이 글에서 나오는 방식보다 더 좋은 방식이 있다면 댓글로 추천 부탁드립니다 :)

오늘은 프로젝트 C에서 자동 공격을 하는 오브젝트를 만들어보려고 한다.

 

해당 오브젝트는 기본적으로 이전 포스팅을 통해 작업한 상태 패턴을 기반으로 동작한다.

https://lms0408.tistory.com/14

 

유니티 프로젝트 개발 일지 - 1 (상태 패턴)

오늘은 프로젝트 C에서 오브젝트의 상태 패턴을 구현해 볼 것이다. 이전 개인 프로젝트에서 상태 패턴을 구현할 땐,Player와 Montser 오브젝트가 있을 때 각기 다른 상태를 정의하고 같은 오브젝트

lms0408.tistory.com

상태는 총 3가지로 Idle(적을 탐색하는 상태), Attack, Dead를 가진다.

오브젝트가 Attack 상태일 때 공격을 할 수 있도록 코드를 작성해보고자 한다.

 

우선 무기의 기초적인 뼈대를 작성해 볼 것이다.

public interface IWeapon
{
    public bool IsAtk { get; }
    public void ReadyAttack(float attackingTime);
    public bool UpdateAttack(Vector2 pos, Vector2 dir, float attackingTime);
}

IsAtk 프로퍼티는 무기의 공격이 준비되어 공격이 가능한 상태를 알리고,

ReadyAttack 메서드는 특정 상태일 때 매 프레임마다 호출되어 공격이 가능하게끔 준비해 주는 메서드,

UpdateAttack도 특정 상태일 때 매 프레임마다 호출되어 공격을 진행해 주는 메서드로 구현할 예정이다.

 

UpdateAttack 메서드가 bool 타입 반환형으로 선언한 이유는

해당 메서드를 호출하는 오브젝트가 가지고 있는 무기가 공격을 했다는 신호를 받아 애니메이션 클립을 재생해주기 위함이다.

 

이젠 선언한 인터페이스를 바탕으로 Weapon 클래스를 구현해보려고 한다.

[System.Serializable]
public abstract class Weapon : IWeapon
{
    [SerializeField] private float atk; // 공격력

    [SerializeField] private int maxAtkCount; // 공격 횟수
    private int atkableCount; // 현재 공격 가능한 횟수

    [SerializeField] private bool isAtk;
    [SerializeField] private float atkElapsed; // 공격 경과 시간

    [SerializeField] private float coolTime = 0.5f; // 쿨타임
    [SerializeField] private float coolTimeElapsed; // 쿨타임 경과 시간
    
    public void ReadyAttack(float attackingTime)
    {
        if ((coolTimeElapsed += Time.deltaTime) < coolTime) return;

        isAtk = true;
        atkElapsed = attackingTime;
    }
    
    public bool UpdateAttack(Vector2 pos, Vector2 dir, float attckingTime)
    {
        if ((atkElapsed += Time.deltaTime) < attckingTime) return false;

        if (--atkableCount < 0)
        {
            coolTimeElapsed = 0f;
            atkableCount = atkCount;
            isAtk = false;
            return false;
        }
    
        atkElapsed = 0f;
        Attack(pos, dir);

        return true;
    }
    
    protected abstract void Attack(Vector2 pos, Vector2 dir);
}

ReadyAttack 메서드는 Idle 상태에서 호출된다.

오브젝트들은 타겟과의 거리가 공격 범위내에 존재해야 그 자리에서 공격을 할 수 있어

이동과 같은 특수한 행동 상태 중에는 호출하지 않는다.

 

내부 구현은 매우 간단하게 되어있다.

호출이 진행됨에 따라 쿨타임 경과 시간을 업데이트해주면서 공격 준비를 시켜준다.

쿨타임이 완료되었을 땐 해당 메서드를 호출한 오브젝트가 공격 상태가 되도록

isAtk true로 변경해준다.

 

ReadyAttack을 통해 오브젝트가 Attack 상태가 되었다면 UpdateAttack 메서드를 호출하여

공격을 진행해 준다.

UpdateAttack 메서드는 atkableCount(공격 가능한 횟수)가 모두 소진되기 전까지 매 프레임 호출되게 된다.

 

매개변수 attackingTime은 한 번의 공격이 진행되는 시간이며

나는 기본값으로 공격 애니메이션 클립의 Length를 대입해 줄 것이다.

(+ ReadyAttack 메서드에서 보면 공격 경과시간을 attackingTime을 대입해 주면서 공격 상태를 맞이하게 되는데

그 이유는 공격 상태가 되었을 때 곧바로 공격을 하기 위함이다.)

 

공격 진행 시간이 초과된 이후

공격 가능한 횟수가 남아있다면 다시 공격을 진행해 준다.

만약 공격 가능한 횟수가 남아있지 않다면 공격 준비 상태가 되도록 isAtkfalse로 변경해 주게 된다.

 

이 Weapon 클래스의 뼈대를 바탕으로 나는 근거리 무기와 원거리 무기로 나누어 상속을 해주었다.

현재는 원거리 무기를 테스트하고자 하니 간단하게 투사체를 발사하는 코드를 구현하면 다음과 같다.

[System.Serializable]
public class LongRangeWeapon : Weapon
{
    public Rigidbody2D pref;
    protected override void Attack(Vector2 pos, Vector2 dir)
    {
        var _obj = GameObject.Instantiate(pref, pos, 
            Quaternion.Euler(0f, 0f, Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg - 90f));
        _obj.velocity = dir * 5f * Time.fixedDeltaTime;
    }
}

 

이제 위의 Weapon 로직이 잘 돌아가는지 확인하기 위해 투사체를 발사하는 대포 오브젝트에 적용을 해보자.

public interface ICannonAction : IIdleAction, IAttackAction, IDeadAction { }

public class CannonController : MyObject, ICannonAction
{
    [SerializeField] private LongRangeWeapon myWeapon;
    [SerializeField] private Animator _animator;
    
    public Quaternion originRot;
    public Transform Target;
    
    // Something...
    
    public void Idle()
    {
        Quaternion _targetRot = Quaternion.identity;
        if (Target == null) _targetRot = originRot;
        else
        {
            // Cannon Position에서 타겟 Position까지의 벡터를 구해 각도를 변환
            var _dir = (weapon.Target.Position - (Vector2)weapon.Transform.position).normalized;
            var _axis = Mathf.Atan2(_dir.y, _dir.x) * Mathf.Rad2Deg;
            _targetRot = Quaternion.Euler(0f, 0f, _axis);

            myWeapon.ReadyAttack(1f);
        }
        
        var _rotSpeed = 10f;
        transform.rotation = Quaternion.Lerp(transform.rotation, _targetRot, Time.deltaTime * _rotSpeed);
    }
    
    public void Attack()
    {
        if (!myWeapon.UpdateAttack(transform.position, transform.up, 1f)) return;
        // Weapon이 공격을 했다면 애니메이션 재생
        _animator.SetTrigger("Attack");
    }
    
    // Something...
}

위 코드는 CannonController의 핵심적인 코드의 일부분이며, 위에서 설명한 설계대로 구현이 된 상태이다.

추가적인 부분은 타겟이 존재하지 않을땐 항상 처음에 셋팅한 rotation을 바라보고,

타겟이 존재하고 공격이 준비되고 있는 상태에선 타겟을 바라보도록 연산해보았다.

 

해당 스크립트 컴포넌트가 적용된 오브젝트를 적절하게 배치하여 테스트 해보면 문제없이 잘 동작하는 모습을 볼 수 있게 된다.

오늘은 프로젝트 B에서 진행한 곡선 UI를 그리는 작업을 공유하고자 한다.

우선 Line을 그리기 위해 유니티 Effects의 Line 오브젝트를 생성해주도록 하자.

오브젝트를 생성한다면 Line Renderer 컴포넌트가 포함되어 있을 것이다.

여기서 우리가 중점으로 봐야할 부분은 Positions 속성이다.

대충 값을 대입하다보면 해당 Position들은 점들의 위치이며 두개의 점을 이어 Line을 그려준다는 것을 알 수 있다.

 

우선 한 정점을 기준으로 마우스 커서 까지 이어주는 직선 형태의 Line을 그려보도록 하자.

public class StraightLine : MonoBehaviour
{
    [SerializeField] private LineRenderer line;
    
    void Start()
    {
        line.positionCount = 2;
        line.SetPosition(0, Vector2.zero);
    }

    void Update()
    {
        var _mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        line.SetPosition(1, _mousePos);
    }
}

직선은 정점 두개로 이루어지기 때문에 positionCount를 2로 초기화 해주었다.

나는 시작점을 (0, 0)으로 설정해주었고 마우스 position을 월드 좌표로 변환하여 끝점에 대입해주었다.

 

하지만 내가 구현해야하는 방식은 직선이 아닌 곡선이다보니 위의 방식과는 다르게 가야한다.

곡선을 구현하기 위한 아이디어를 찾기 위해 인터넷 검색을 통해 알아본 결과 베지어 곡선을 이용해야한다는 것을 알게 되었다.

 

베지어 곡선이란? n개의 조절점을 가지고 선형 보간을 통해 곡선 형태를 이루는 점들의 집합을 구하는 공식이다.

베지어 곡선에 대한 자세한 내용은 아래의 블로그를 참고하자!

https://ko.javascript.info/bezier-curve

 

베지어 곡선

 

ko.javascript.info

해당 프로젝트에서 그리는 곡선은 이차 함수의 모양 형태로 그려야하기 때문에 조절점이 3개인 2차 베지어 곡선의 식을 참고하여 구현해보았다.

https://namu.wiki/w/%EB%B2%A0%EC%A7%80%EC%97%90%20%EA%B3%A1%EC%84%A0

위의 식을 참고하여 코드로 나타내면 다음과 같다.

public int segmentCount = 20;
public float controlPointY = 1f;

private Vector2 CalculateQuadraticBezierPoint(Vector2 p1, Vector2 p2, Vector2 p3, float t)
{
    var _u = 1 - t;
    var _tt = t * t;
    var _uu = _u * _u;

    Vector2 _point = _uu * p1;
    _point += 2 * _u * t * p2;
    _point += _tt * p3;

    return _point;
}

private void DrawBezierCurve(Vector2 endPos)
{
    var _positions = new Vector3[segmentCount + 1];
    line.positionCount = segmentCount + 1;

    var _p1 = Vector3.zero;
    var _p3 = endPos;

    var _p2 = new Vector3((_p1.x + _p3.x) * 0.5f, _p1.y + controlPointY, 0f);

    for (int i = 0; i < segmentCount + 1; i++)
    {
        float _t = (float)i / segmentCount;
        _positions[i] = CalculateQuadraticBezierPoint(_p1, _p2, _p3, _t);
    }

    line.SetPositions(_positions);
}

p1과 p3는 각각 시작점과 끝점으로 대입해주었으며 p2 조절점의 경우 p1과 p3의 중점으로 두었다.

점의 갯수가 많을 수록 더 부드러운 곡선을 연출할 수 있으므로 20개 정도로 임의의 값을 넣어 실행해보았다.

실행한 결과 만족스러운 결과가 나왔다.

여기서 조절점의 위치를 조절해서 원하는 곡선 형태로 만들어준다면 더 이쁘게 나올거 같다.

부족함이 많은 코드인만큼 더 좋은 방식이 있다면 댓글로 추천해주시면 감사하겠습니다!

오늘은 A 프로젝트에서 같이 개발하는 팀원에게 이슈 관련으로 연락이 왔다.

유니티 Editor Game뷰에서 스프라이트의 레이어 순서가 적용되지 않는 문제가 발생한 것이다.

 

오브젝트의 전체 몸체를 개별로 나누어서 파츠 형식으로 리소스를 제작한 상태이고

각 파츠별 앞서 있는 sprite 일수록 Order in Layer 값을 조절한 상태였다.

원래는 왼쪽과 같이 설정한 Layer의 순서대로 Rendering 되어 이쁘게 나와야 하는 오브젝트가...

오른쪽과 같이 Rendering 순서가 잘못되어 이쁘게 나오지 않는 현상이 발생한 것이다

설정한 Layer 순서가 적용이 되지 않았는지 확인하기 위해 Frame Debugger를 통해 Draw 되는 순서를 확인해 보았지만

설정한 순서대로 잘 Draw가 되고 있었다...

 

해당 오브젝트는 2.5D의 분위기를 연출하는 씬에서 사용되기 때문에 씬에서 Game뷰를 랜더링 해주는 카메라 projection이 perspective로 설정되어 있어 급한 대로 z축의 값을 일부 조정해서 렌더링 순서를 맞췄다.

 

하지만 z축 값이 동일한 상태에서 Layer 값만을 조절해서 해결할 수 없는 부분이 매우 의문이 들었다.

그래서 오늘은 왜 Layer 값만으론 해결할 수 없었는지

z축 값을 조절하지 않고도 랜더링 순서 대한 문제를 해결할 수 있는 방법에 대해 알아보고자 한다.


여러 방법을 시도해 보면서 문제에 대한 삽질을 한 끝에 무엇 때문에 해당 문제가 발생되었는지 알게 되었다.

결론적으로 말하자면 Z-fighting이 발생해서 위와 같은 문제가 발생한 것이다.

 

https://drehzr.tistory.com/1461

Z-fighting이란 두 오브젝트가 동일한 깊이를 공유할 때 Z-buffer의 정밀도 부족으로 인해 렌더링 결과가 깜빡이거나 섞이는 현상이다.

 

Orthographic모드는 카메라와 오브젝트 간의 실제 거리 대신 직선 거리를 기반으로 선형적으로 계산되어 Z-buffer의 정밀도가 높아지는 데에 비해

Perspective모드는 실제 거리를 기반으로 멀어질수록 Z-buffer의 정밀도가 떨어지게 된다.

 

그렇기 때문에 오브젝트의 rotation이 카메라를 바라보는 각도라면

Z-buffer의 정밀도가 높아져 Z-fighting의 발생 빈도가 줄어들면서 Z값을 수정하지 않더라도 위의 문제를 해결할 수 있을 것이다.

 

하지만 오브젝트를 항상 카메라를 바라보도록 하는 방식은 현재 게임 방향성과 맞지 않는 부분이기 때문에 다른 방식을 택하여 이 문제를 해결했다.

현재 Z-fighting이 발생하는 오브젝트의 Depth를 읽지 않도록 설정한다면 rotation을 수정하지 않고도 Layer 순서대로 Game뷰에 랜더링 될 것이다.

기존 사용하던 Shader에서 Depth Write를 Disabled로 변경해주면 깊이를 읽지 않을 수 있다.

해당 설정으로 변경한 결과 기존 의도했던 순서대로 Sprite들이 랜더링 되는 모습을 볼 수 있다.

 


참고 블로그 : https://rito15.github.io/posts/unity-transparent-stencil/

오늘은 프로젝트 A에서 사용할 팝업 UI 컨트롤러를 만들어볼 생각이다.

 

팝업이 단순히 하나씩만 표시되게 한다면 컨트롤러가 굳이 필요하지 않을거라 생각했는데

게임의 방향성을 보았을 때 여러 팝업을 열기로 결정이 되어 제작하기로 했다.

 

우선 기본적으로 새로운 팝업이 열렸을 때 이전 팝업의 상호작용은 불가능해야하며,팝업을 닫을 땐 먼저 열린 팝업이 제일 마지막에 닫혀야한다.

이러한 점을 고려했을 때 후입선출의 특징을 가지고 있다는 것을 알 수 있다.

그렇기 때문에 후입선출의 자료구조인 Stack을 활용하여 컨트롤러를 제작해보았다.

 

우선 한 컨트롤러로 모든 PopUp들을 컨트롤하기 위해선 캡슐화가 필요했다.

public interface IPopUp
{
    public void Active();
    public void UnActive();
    public void ActiveRayTarget();
    public void UnActiveRayTarget();
}

ActiveUnActive를 통해 팝업 오브젝트의 Active 상태를 컨트롤 하고 ActiveRayTargetUnActiveRayTarget을 통해 팝업 오브젝트의 상호작용을 컨트롤 해주기 위해 이렇게 4가지의 메서드를 캡슐화 해보았다.

그리고 정의된 인터페이스를 통해 팝업 컨트롤이 필요한 클래스에 적절히 인터페이스를 구현해주었다.

public class ExplainCanclePopUp : MonoBehaviour, IPopUp
{
    // something...
    public void Active()
    {
        StartCoroutine(FadeIn());
    }
    
    public void UnActive()
    {
        StartCoroutine(FadeOut());
    }
    
    public void ActiveRayTarget()
    {
    	img.raycastTarget = true;
    }
    
    public void UnActiveRayTarget()
    {
    	img.raycastTarget = false;
    }
    
    private IEnumerator FadeIn()
    {
        // Something...
    }
    
    private IEnumerator FadeOut()
    {
    	// Something...
    }
}

나는 취소 상호작용을 설명해주는 PopUp UI에 인터페이스를 구현해주었다.

팝업이 열릴 땐 Fade In이 되고, 팝업이 닫힐 땐 Fade Out이 되도록 구현해주었고

raycastTarget의 값을 변경하여 상호작용 컨트롤을 구현해주었다.

 

다음은 위와 같이 구현된 여러 개의 팝업 UI를 컨트롤 해줄 PopUpController를 구현해볼려고 한다.

public class PopUpController : MonoBehaviour
{
    private Stack<IPopUp> popUpStack = new Stack<IPopUp>();
    
    public void PushPopUp(IPopUp popUp) { }
    public void PopPopUp() { }
}

앞서 설명했듯이 후입선출의 특징으로 팝업 리스트를 관리해주기 위해 Stack 자료구조로 정의해주었다.

또한 팝업을 삽입, 삭제를 할 수 있는 간단한 메서드도 정의해주었다.

삽입에 대한 구현은 다음과 같다.

public void PushPopUp(IPopUp popUp)
{
    if (popUpStack.Count > 0)
    {
        var _top = popUpStack.Top();
        
        if (_top.Equals(popUp)) return;
        _top.UnActiveRayTarget();
    }
    
    popUpStack.Push(popUp);
    popUp.Active();
    popUp.ActiveRayTarget();
}

public static class ExtensionMethods
{
    public static T Top<T>(this Stack<T> values)
    {
	var _top = values.Pop();
   	values.Push(_top);
   	return _top;
    }
}

Stack에 팝업을 Push하기전 열려있는 팝업이 존재하는지 체크를 해주고 있다면 해당 팝업의 상호작용을 하지 못하도록 제어해준다.

이후 새로 열린 팝업을 리스트에 Push 해주고 해당 팝업을 Active하고 상호작용이 가능하도록 해준다.

 

+ C# Stack 자료구조는 Pop 메서드를 통해 마지막에 대입된 값을 반환 받을 수 있기 때문에

데이터의 삭제를 원치 않을 경우엔 번거롭게 다시 추가해주는 작업을 거쳐야 된다.

그렇기 때문에 나는 기존 Stack 클래스의 확장 메서드로 Top을 정의해주었다.

 

삭제에 대한 구현은 다음과 같다.

public void PopPopUp()
{
    if (popUpStack.Count.Equals(0))
    {
        PushPopUp(optionPopUp);
        return;
    }
    
    var _pop = popUpStack.Pop();
    _pop.UnActiveRayTarget();
    _pop.UnActive();
    
    popUpStack.Top().ActiveRayTarget();
}

private void Update()
{
    if (Input.GetKeyDown(KeyCode.Escape)) PopPopUp();
}

삭제 구현도 삽입과 마찬가지로 간단하다.

삭제에선 닫히는 팝업을 제어해주고 닫힌 팝업 이전에 있는 팝업을 활성화 시켜주면 된다.

 

이 프로젝트에선 팝업 닫기 버튼 이외에도 ESC키를 이용하여 팝업을 닫을 수 있도록 하였기 때문에

Update문에서 Input을 체크해주었고,

아무런 팝업이 열려있지 않을 때 ESC 키를 상호작용하면 옵션 팝업이 열리도록 구현해주었다.

 

이렇게 하나의 팝업 컨트롤러를 통해 관리하니 비교적 코드도 많이 쓰지 않고 구현한 것 같다.

더 좋은 방식이 있다면 댓글로 추천해주시면 감사하겠습니다!

오늘은 프로젝트 C에서 오브젝트의 상태 패턴을 구현해 볼 것이다.

 

이전 개인 프로젝트에서 상태 패턴을 구현할 땐,Player와 Montser 오브젝트가 있을 때 각기 다른 상태를 정의하고 같은 오브젝트 끼리는 해당 상태 인스턴스를 공유한 방식이었다.같은 상태를 가지는 오브젝트가 개별로 상태 인스턴스를 가지고 있는건 메모리적으로 낭비가 되는 부분이기 때문에각 상태를 CashData로 저장하여 공유가 되도록 했다.

 

이번에는 이전 방식과 로직은 비슷하되 Scriptable Object를 활용하여 제작해보기로 결정했다.

 

스크립트는 크게 세 분류로 나누었으며,각 상태를 컨트롤 해주는 State Machine과 해당 상태로 변경할 수 있는 지에 대한 여부를 판단하는 Decision,해당 상태의 Action이 정의된 State로 나누었다.

public abstract class StateDecision : ScriptableObject
{
    public abstract bool Decision(MyObject obj);
}

Decision은 bool 반환형 메서드로 정의하여 해당 상태로 변경이 가능한지에 대한 여부를 판단할 수 있도록 정의해주었다.

public abstract class State : ScriptableObject
{
    public StateDecision decision;

    public virtual void Enter(MyObject obj) { }
    public virtual void Action(MyObject obj) { }
    public virtual void Exit(MyObject obj) { }
}

State는 StateDecision 필드 변수 하나와 메서드 3가지로 정의하였다.

decision은 앞서 정의한 StateDecision을 가지는 필드 변수이다. 각 상태에 대응되는 Decision을 대입할 것 이다.

Enter는 해당 상태로 변했을 때 초기화가 이루어지는 부분이고, Action은 해당 상태에서 매 프레임마다 Update되는 메서드이다.

Exit는 해당 상태가 종료되었을 때 한번 호출되는 메서드이다.

public class GeneralStateMachine : ScriptableObject
{
    [SerializeField] private List<State> states;

    public void TransState(MyObject obj, ref State curState)
    {
        foreach (var _state in states)
        {
            if (_state.decision == null || _state.decision.Decision(obj))
            {
                ChangeState(obj, ref curState, _state);
                break;
            }
        }
    }

    public void UpdateState(MyObject obj, State curState)
    {
        curState.Action(obj);
    }

    private void ChangeState(MyObject obj, ref State curState, State newState)
    {
        if (curState.Equals(newState)) return;

        curState.Exit(obj);
        newState.Enter(obj);
        curState = newState;
    }
}

StateMachine은 매 프레임마다 호출되어 상태를 바꾸어주는 TransState와 매 프레임마다 현재 상태의 Action을 호출해주는 UpdateState로 나누었다.

 

각 상태는 List를 통해 저장하며 순서가 젤 앞설수록 상태 전환의 우선 순위를 가질 수 있도록 설계했다.

 

현재 상태는 각 오브젝트 별로 가지고 있어(현재 상태는 고유의 값이므로) 매개변수를 통해 현재 상태를 전달해준다.

이때 현재 상태를 매개 변수로 전달할 때 값 형식으로 전달되기 때문에 반드시 ref 키워드를 붙여 참조 형식으로 전달해주어야 현재 상태의 값이 반영된다.

 

이렇게 뼈대는 완성이 되었고 이제 살만 붙이는 작업만 해준다면 멋진 상태 패턴을 만들 수 있다.

 

이전 방식에선 Controller에서 상태에 대한 행동을 정의하지 않고 상태 클래스 내에서 행동을 모두 정의했다.

Controller의 코드 다이어트가 되어 조금 더 깔끔해 보이지 않을까 하는 생각에 해당 방식으로 작성한 것이었다.

 

하지만 이번엔 조금 더 상태를 범용성있게 사용하기 위해 각 Controller에 상태에 대한 행동을 정의하고 해당 메서드를 각 상태에서 호출할 수 있게끔 설계하였고 먼저 캡슐화 작업을 해주었다.

#region State
public interface IIdleState
{
    public bool Idleable { get; }
}
public interface IMoveState
{
    public bool Moveable { get; }
}
public interface IAttackState
{
    public bool Attackable { get; }
}
public interface IHitState
{
    public bool Hitable { get; }
}
public interface IDeadState
{
    public bool IsDead { get; }
}
#endregion

#region Action
public interface IIdleAction
{
    public void Idle();
}
public interface IMoveAction
{
    public void Move();
}
public interface IAttackAction
{
    public void Attack();
}
public interface IHitAction
{
    public void Hit();
}
public interface IDeadAction
{
    public void Dead();
}
#endregion

캡슐화는 Decision에 필요한 것과 Action을 위해 필요한 것으로 나누어 정의했다.이후 필요한게 있다면 추가적으로 캡슐화를 할 예정이다.

 

이렇게 정의된 interface를 바탕으로 각 State와 Decision을 정의해주었고, 아래는 Move 상태에 대한 코드이다.

public class MoveDecision : StateDecision
{
    public override bool Decision(MyObject obj)
    {
        return (obj as IMoveState).Moveable;
    }
}

public class MoveState : State
{
    public override void Enter(MyObject obj)
    {
    }
    public override void Action(MyObject obj)
    {
        (obj as IMoveAction).Move();
    }
    public override void Exit(MyObject obj) 
    {
    }
}

매개 변수를 통해 전달 받은 오브젝트를 캐스팅하여 메서드를 호출하는 간단한 코드이다.

 

이렇게 정의된 스크립트를 바탕으로 Decision과 State를 만들어주었고, 해당 State에 대응되는 Decision을 대입해주었다.

다른 상태들 역시 필요하다면 이렇게 추가하여 관리할 수 있다.

 

이렇게 몬스터에 필요한 각 상태들을 정의하고 우선순위에 맞게 각 상태들을 List에 대입한 모습이다.

 

이 방식은 Editor상에서 좀 더 직관적으로 상태를 관리할 수 있다는 점이 아주 좋은 부분인 것 같다.

처음 시도인만큼 완벽한 설계 방식은 아니지만 문제없이 돌아간다는 점에서 큰 만족감을 가지게 되었다.

현재 유니티를 활용하여 게임 프로젝트에 참가하고 있는 상태이다.

총 3개의 프로젝트를 진행하고 있으며, 개발 일지를 작성할 땐 각각의 프로젝트 이름을 A, B, C라고 지칭해서 포스팅을 할려고한다.

 

3개의 프로젝트 모두 내년 상반기내에 출시할 예정이며

출시 하기전까지 내가 개발한 기록을 포스팅할 예정이다. (일지인 만큼 매일 하루에 하나씩 올릴 수 있도록 노력할려고 한다!)

 

처음에는 이러한 개발일지를 작성하고자 하는 마음이 없었다. 

이전까지는 가벼운 내용들은 머릿속에만 넣고 기록을 하지 않을려고 했지만,

시간이 지나면 휘발될 가능성이 높기 때문에 기록하고자 결심했다!

 

앞으로 작성될 개발 일지 내용에 대해 많은 관심 부탁드립니다 :)

이전 게시글에서 Blocking과 Non-blocking의 차이와 Synchronous와 Asynchronous의 차이에 대해서 정리해봤다.

 

Synchronous vs Asynchronous & Blocking vs Non-blocking

Synchronous와 Asynchronous, Blocking과 Non-blocking이라는 말은 운영체제(OS)를 공부하면서 듣게 된 개념이다.프로그래밍을 함에 있어 중요한 개념인 만큼 이번 기회에 정리를 해보고자 한다.Synchronous와 Asyn

lms0408.tistory.com

 

비동기와 동기 및 블럭킹과 논-블럭킹에 대한 최종 이야기 남아있다.

https://homoefficio.github.io/2017/02/19/Blocking-NonBlocking-Synchronous-Asynchronous/

바로 위와 같이 2대2 Matrix로 정리된 표이다.

위의 표에 정리된 그림을 보고 이해가 된다면 해당 내용을 알고 있는 것이다.

해당 내용이 이해가 되지 않는다면 이 게시글을 보면서 이해해보도록 하자!


Sync Blocking과 Async Non-blocking

Sync Blocking

https://homoefficio.github.io/2017/02/19/Blocking-NonBlocking-Synchronous-Asynchronous/

Sync-Blocking은 말그대로 Blocking이 이루어지는 동기 방식이다.

위의 그림을 보면 호출이 이루어짐에 따라 작업이 완료되기 전까지

제어권을 일시적으로 위임하는 Blocking의 특성이 보이며

작업이 완료되는 즉시 해당 작업에 대한 처리를 진행하는 Sync의 특성이 돋보이는 방식이다.

 

작업의 결과가 나오기 전까지 다른 작업을 진행할 수 없는만큼 전체 작업 수행 시간이 오래 걸린다.

Sync Blocking Example

간단한 예시로는 위와 같이 식당에서 웨이터가 주문을 받고 요리사에게 음식을 만들어달라고 요청했을 때

요리가 나오기 전까지 웨이터는 아무런 일도 하지 않고 음식이 나오기까지 기다리는 상황과 같다.

 

이렇게 식당에서 일을 하게 되면 주문 한건에 소요되는 시간이 큰 만큼 하루에 음식을 제공하는 양은 적어질 것 이다.

그렇게 된다면 당연히 매출이 떨어질 것 이기때문에 비효율적인 방식이라고 볼 수 있다.

Async Non-blocking

https://homoefficio.github.io/2017/02/19/Blocking-NonBlocking-Synchronous-Asynchronous/

Async Non-blocking은 Sync Blocking과 정반대되는 방식이다.

필요에 따라 호출을 진행한 이후에도 제어권을 잃지 않기 때문에 Non-blocking 특성이 보이며

작업이 완료되었을 때 즉시 해당 결과에 대한 상황을 처리할지 안할 지는 불명확하다는 면에서

Async의 특성이 보인다.

 

작업의 결과를 기다리지 않고 다른 작업을 수행할 수 있는만큼 전체 작업 수행 시간이 단축된다.

Async Non-blocking Example

Sync Blocking과 마찬가지로 비슷한 상황이라고 가정할 때

Async Non-blocking방식은 요리사가 요리를 하는 동안 청소를 한다던지 다른 손님의 주문을 받는다던지 등

음식을 기다리지 않고 다른 일을 할 수 있다.

또한 음식이 나오더라도 상황에 따라 즉시 서빙을 할지 안할지는 모른다.

위의 상황은 음식이 나왔더라도 다른 손님의 주문을 받고 있어 서빙이 나중에 이루어지는 모습이다.

 

Sync Blocking과는 다르게 주문 한건에 시간이 소요되는 동안 다른 주문도 받기때문에 하루에 음식을 제공하는 양이 앞선 방식보단 많아질 것 이다.당연히 매출은 증가할 것 이고 효율적인 방식이라고 볼 수 있다.

 

위의 두 방식은 우리가 프로그램을 만들 때 익숙하게 이용한 방식이다.

 

Sync Blocking의 예시를 들자면

C언어에서 사용자의 입력값에 따라 출력되는 프로그램이라고 볼 수 있다.

Async Non-blocking의 경우 

Unity C#에서 제공하는 Addressables API를 이용하여

번들을 로드한 이후 Completed 콜백을 통해 추가적인 작업을 처리할 때로 볼 수 있다.


Sync Non-blocking과 Async Blocking

Sync Non-blocking

https://homoefficio.github.io/2017/02/19/Blocking-NonBlocking-Synchronous-Asynchronous/

Sync Non-blocking은 제어권을 잃지 않는 것이다.

호출을 통해 작업을 요청했어도 작업이 완료되는 동안 다른 작업을 수행할 수 있다.

다만 다른 일을 수행하면서도 작업이 완료되었는지 중간 중간 확인을 하며

확인 중 작업이 완료되었다는 회신을 통해 결과를 즉시 처리한다.

Sync Non-blocking Example

이번에는 주문에 대한 요리를 요청한 뒤 화장실 청소를 한다고 가정을 해보자.

화장실과 주방에 거리는 멀기 때문에 음식이 나오는 것을 직접 가서 확인해야한다.

확인할 때마다 요리가 나오지 않았다면 다시 청소를 하러 돌아갈 것 이고

요리가 나왔다면 즉시 서빙을 할 것 이다.

Async Blocking

https://homoefficio.github.io/2017/02/19/Blocking-NonBlocking-Synchronous-Asynchronous/

 

Async Blocking은 제어권을 다른 작업에게 위임하는 것 이다.

호출을 통해 작업을 요청했을 경우 제어권을 위임하기 때문에  제어권을 다시 얻기 전까진

다른 작업을 수행하지 못하고 대기해야한다.

Async Blocking Example

마찬가지로 비동기식이므로 완료된 작업에 대해 즉시 처리 여부는 불명확하다.

이번엔 가게에서 정해진 break time으로 인해 잠시 가게를 닫은 다음

이 시간을 활용해 웨이터는 요리사에게 점심 식사를 요구한 상태라고 가정하자.

 

요리사가 점심 식사를 준비할 때까지 심심한 나머지 웨이터에게 말동무가 되어달라고 한다.

요리사는 본인의 옆에서 이야기를 하는 걸 좋아하기때문에 웨이터는 다른 일을 하지 못하게 된다.

점심 식사 준비를 완료가 된 이후 자유가 된 웨이터는 점심 식사를 바로 할지

화장실에서 손을 씻고 밥을 먹을지는 알 수가 없다.

 

위의 두 방식은 Sync Blocking 및 Async Non-blocking과 다르게 우리가 흔히 이용하는 방식은 아니다.

그렇기 때문에 예시를 통해 이해가 되었더라도 이 방식들이 어떻게 실제로 적용된 사례가 있을지는 감이 오지 않을 수도 있다.

 

Sync Non-blocking의 경우 게임의 로딩 progress를 예시로 들 수 있다.

로딩이 진행되는 동안 수시로 어느 정도 진행이 되었는지 확인을 하며 UI를 업데이트하기 때문이다.

Async Blocking의 경우는 

비효율적인 면이 크게 나타나므로 보통은 해당 의도를 가지고 구현하는 경우는 거의 없다.

보통 의도를 Async Non-blocking으로 하려다 개발자의 실수 혹은 기타 이유로 인해 일어난다.


마무리

이전부터 정말 헷갈려 하는 개념을 제대로 이해하고 정리하게 되면서 마음이 많이 홀가분해졌다.

Sync 및 Async와 Blocking과 Non-blocking은 명확히 다른 개념이며

이 두 부류는 공존 할 수 있다는 것을 알 수 있다.


참고 영상 : https://www.youtube.com/watch?v=oEIoqGd-Sns&list=LL&index=1&t=528s

+ Recent posts