24-05

Sandy·2024년 6월 11일

[Today I Learned]

목록 보기
17/18

240503_Serialization

Serialization

public override void OnConnected(EndPoint endPoint)
{
    PlayerInfoReq packet = new PlayerInfoReq() { packetId = (ushort)PacketID.PlayerInfoReq, playerId = 1001 };

    // Send
    ArraySegment<byte> s = SendBufferHelpher.Open(4096);

    bool success = true;
    ushort count = 0;

    count += 2;
    success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset + count, s.Count - count), packet.packetId);
    count += 2;
    success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset + count, s.Count - count), packet.playerId);
    count += 8;

    success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset, s.Count), count);

    ArraySegment<byte> sendBuff = SendBufferHelpher.Close(count);
    if (success)
    {
        Send(sendBuff);
    }
}

Deserialization

public override void OnRecvPacket(ArraySegment<byte> buffer)
{
    ushort count = 0;

    ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
    count += 2;
    ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count);
    count += 2;

    switch ((PacketID)id)
    {
        case PacketID.PlayerInfoReq:
            {
                long playerId = BitConverter.ToInt64(buffer.Array, buffer.Offset + count);
                count += 8;
                Console.WriteLine($"PlayerInfoReq {playerId}");
            }
            break;
    }

    Console.WriteLine($"RecvPacket ID: {id}, Size {size}");
}

240508_InputSystem

InputSystem

입력받은 키 바인딩해서 액션 실행하기

public class TopDownController : MonoBehaviour
{
    public event Action<Vector2> OnMoveEvent;
    public event Action<Vector2> OnLookEvent;

    public void CallMoveEvent(Vector2 direction)
    {
        OnMoveEvent?.Invoke(direction);
    }

    public void CallLookEvent(Vector2 direction)
    {
        OnLookEvent?.Invoke(direction);
    }

}
public class PlayerInputController : TopDownCharacterController
{
    private Camera _camera;
    private void Awake()
    {
        _camera = Camera.main;
    }

    public void OnMove(InputValue value)
    {
        // Debug.Log("OnMove" + value.ToString());
        Vector2 moveInput = value.Get<Vector2>().normalized;
        CallMoveEvent(moveInput);
    }

    public void OnLook(InputValue value)
    {
        // Debug.Log("OnLook" + value.ToString());
        Vector2 newAim = value.Get<Vector2>();
        Vector2 worldPos = _camera.ScreenToWorldPoint(newAim);
        newAim = (worldPos - (Vector2)transform.position).normalized;

        if (newAim.magnitude >= .9f)
				// Vector 값을 실수로 변환
        {
            CallLookEvent(newAim);
        }
    }

    public void OnFire(InputValue value)
    {
        Debug.Log("OnFire" + value.ToString());
    }
}

240509_ScriptableObject

ScriptableObject

데이터를 따로 빼놔서 에셋으로 저장하기

언리얼 Data Table 하고 비슷하다

[CreateAssetMenu(fileName = "DefaultAttackSO", menuName = "TopDownController/Attacks/Default", order = 0)]

public class AttackSO : ScriptableObject
{
    [Header("Attack Info")]
    public float size;
    public float delay;
    public float power;
    public float speed;
    public LayerMask target;

    [Header("Knock Back Info")]
    public bool isOnKnockBack;
    public float knockBackPower;
    public float knockBackTime;

}

Q 게임 실행하면 화살이 투명해져서 안보인다?

PlayerRangeAttack SO에서 데이터 넣을 때 Projectile Color에 알파가 0으로 들어가 있었다.

화살 생성하는 부분에 브레이크 걸고 씬에서 화살 확인했다. 프리팹에서는 제대로 하얀색인데 실행하면 알파가 0이 된다.

Q InputSystem에서 OnFire 하면 실행되는 코드 흐름?

Player에 PlayerInput컴포넌트가 있다

PlayerInput Actions에다가 만들어놓은 InputAction을 연결한다

OnMove, OnLook, OnFire을 호출한다

// PlayerInputController
public void OnFire(InputValue value)
{
    IsAttacking = value.isPressed;
}

// TopDownController
public event Action<AttackSO> OnAttackEvent;

private void Update()
{
    HandleAttackDelay();
}

private void HandleAttackDelay()
{
    if(timeSinceLastAttack < stats.CurrentStat.attackSO.delay)
    {
        timeSinceLastAttack += Time.deltaTime;
    }
    else if (IsAttacking && timeSinceLastAttack > stats.CurrentStat.attackSO.delay)
    {
        timeSinceLastAttack = 0f;
        CallAttackEvent(stats.CurrentStat.attackSO);
    }
}

private void CallAttackEvent(AttackSO attackSO)
{
    OnAttackEvent?.Invoke(stats.CurrentStat.attackSO);
}

// TopDownShooting
private void Start()
{
    controller.OnAttackEvent += OnShoot;
    controller.OnLookEvent += OnAim;
}

private void OnShoot(AttackSO attackSO)
{
    RangedAttackSO rangedAttackSO = attackSO as RangedAttackSO;
    if (rangedAttackSO == null) return;

    float projectileAngleSpace = rangedAttackSO.multipleProjectilesAngle;
    int numberOfProjectilesPerShot = rangedAttackSO.numberOfProjectilesPerShot;

    float minAngle = - (numberOfProjectilesPerShot / 2f) * projectileAngleSpace * 0.5f * rangedAttackSO.multipleProjectilesAngle;

    for(int i = 0; i < numberOfProjectilesPerShot; i++)
    {
        float angle = minAngle + i * projectileAngleSpace;
        float randomSpread = Random.Range(-rangedAttackSO.spread, rangedAttackSO.spread);
        angle += randomSpread;
        CreateProjectile(rangedAttackSO, angle);
    }
}

private void CreateProjectile(RangedAttackSO rangedAttackSO, float angle)
{
    GameObject obj = Instantiate(projectile);
    obj.transform.position = projectileSpawnPosition.position;
    ProjectileController attackController = obj.GetComponent<ProjectileController>();
    attackController.InitalizeAttack(RotateVector2(aimDirection, angle), rangedAttackSO);
}

// ProjectileController
public void InitalizeAttack(Vector2 direction, RangedAttackSO attackData)
{
    this.direction = direction;
    this.attackData = attackData;
    isReady = true;

    UpdateProjectileSprite();
    trailRenderer.Clear();
    currentDuration = 0;
    spriteRenderer.color = attackData.projecileColor;

    transform.right = this.direction;
}

Q 만들어져있는 애니메이션을 갖고왔는데 sprite missing이 뜬다?

Animator 컴포넌트가 MainSprite가 아닌 Goblin에 있었다. Animation 에셋을 만들 때 Goblin에다가 넣고 만들었나보다. 계층구조 상에서 상대적인 위치가 잘못되어 있었다.

240510_LateUpdate

Q 펭귄의 움직임 부분에서 _rigidbody2D.velocity = movementDirection를 하는데 값이 100 이상이 찍힌다?

계속 곱해지는 부분에서 이상하다. 값이 누적해서 곱해지고 있다.

Q movementDirection이 누적해서 곱해지고 있던 이유?

Vector2가 Struct 였다.

처음 만들었던 버전에서 ApplyMovement를 호출해서 인자로 movementDirection을 넘긴다. 그리고 speed랑 곱해서 velocity를 설정해 준다. 두번째 버전으로 함수 호출 안하고 FixedUpdate 부분에서 바로 코드 넣었다. movementDirection 은 멤버변수고 direction은 지역 변수다.
Vector2가 class인 줄 알고 코드는 문제 없다고 생각했다. 왜냐하면 new로 호출하기 때문에 너무 당연히 힙에 만들어지고 함수호출할 때 힙주소 넘길 줄 알았다. 그런데 struct였다. 그래서 1에서 direction은 스택에 값이 새로 만들어진 거여서 변경해도 movementDirection에 영향을 주고있지 않았다.

기존 코드가 잘 작동하니까 잘 써놨겠지~ 하면서 복붙하고 이상한 점을 늦게 알아차렸다.

private Vector2 movementDirection = Vector2.zero;

private void FixedUpdate()
{
    ApplyMovement(movementDirection);
}

// 1
private void ApplyMovement(Vector2 direction)
{
    direction = direction * speed;
    movementRigidbody.velocity = direction;

    Debug.Log(direction);
}

// 2
private void ApplyMovement(Vector2 direction)
{
    movementDirection = movementDirection * speed;
    _rigidbody2D.velocity = movementDirection;

    Debug.Log(movementDirection);
}

Q CameraController에서 LateUpdate에서 카메라 위치를 수정하는데 Player가 흔들리며 움직인다?

Player - FixedUpdate 에서 velocity를 이용해 움직인다.

Camera - LateUpdate에서 player의 위치에 따라 Lerp로 부드럽게 움직인다.

업데이트 프레임이 안맞는다 → 플레이어가 이상하게 움직인다.

해결방법

1 Lerp를 뺀다. 카메라 부드럽게 안움직인다.

2 Camera를 FixedUpdate로 바꿔서 맞춘다 - 카메라 Lerp 움직임 가능

3 Camera - LateUpdate에서 플레이어 위치에 따라 이동 / Player - FixedUpdate에서 위치 이동, RigidbodyInterpolation 값 None -> Interpolate 으로 선택

LateUpdate

3번이 맞는 해결 방법 같다. 왜냐하면 카메라 이동은 모든 오브젝트의 Update가 끝난 후에 찍어야 한다. 그래서 LateUpdate에서 마지막에 카메라 이동처리를 해준다.

https://docs.unity3d.com/ScriptReference/MonoBehaviour.LateUpdate.html

Rigidbody interpolation mode

보간은 물리 타임스텝 업데이트 사이에 해당하는 프레임에서 리지드바디의 포즈를 계산하여 눈에 보이는 지터를 줄입니다. 플레이어 캐릭터 게임 오브젝트 및 카메라가 따라가는 다른 게임 오브젝트에 특히 유용합니다. 기본적으로 보간은 비활성화되어 있습니다.

https://docs.unity3d.com/ScriptReference/RigidbodyInterpolation.html

240513_Q

Q InputSystem, UI를 비주얼 스튜디오에서 못찾았다. References 탭에서도 노란색 경고로 뜬다?

해본 것

유니티 에셋에서 Reimport All 하기 X

폴더에서 Library, obj, Temp 지우기 X

.csproj지우기, Refresh를 눌러서 References 다시 찾기 O

240514_IReflect

Q 캐릭터 선택하는 부분에서 Sprite 2개 넣어서 SetActive로 처리했는데 잘못된 구현 같다.

Animator Override Controller로 구현해서 개선할 수 있다.

BlentTree 언리얼에서 블렌드 스페이스 애니메이션과 비슷하게 기능이 확장되어 있는 것 같다.

Q UnityEngine.GameObject은 타입이 맞는데 where T : 에 못 들어간다?

protected void Bind<T>(Type type) where T : UnityEngine.GameObject

GameObject는 sealed 되어있어서 상속해서 못 쓴다. 그래서 제너릭 필요가 없다.

public sealed class GameObject : Object

UI들은 GameObject가 아니라 Object에서 상속받았다.

UnityEngine.UI.~~, UnityEngine.GameObject, TMPro.TextMeshProUGUI 상위에 Object가 있다.

Q Enum을 넣어서 런타임에 데이터를 가져오는 방법?

// UI_NamePopup

enum GameObjects
{
    InputField
}

public override bool Init()
{
    if (base.Init() == false)
    {
        return false;
    }

    BindObject(typeof(GameObjects));

    return true;
}

// UI_Base
protected void BindObject(Type type) { Bind<GameObject>(type); }

protected void Bind<T>(Type type) where T : UnityEngine.Object
{
    string[] names = Enum.GetNames(type);
}
	

Type이 IReflect 기능을 갖고있다

public abstract class Type : MemberInfo, IReflect {}

public interface IReflect {}

240516_Q

Q TextMeshPro 버튼 한글 못 넣는다?

한글 폰트 받아서 에셋 생성 해준다. Update Atlas Texture 눌러서 CharacterSet을 Custoum Range로 바꾸고 한글이 지원하는 범위에 맞게 누른다. 그래서 그 생성된 에셋 폰트를 쓴다.

Q UI 동적 생성?

@UI_Root 만들고 거기 밑에 다 넣어버린다 프리팹으로 만들어 놓은 UI 부른다.

240517_SoundManager

Q 버튼 입력이 안받아진다?

EventSystem이 없었다. Input 처리, Raycasting, 이벤트 보내주는 역할을 한다.

https://docs.unity3d.com/2018.2/Documentation/ScriptReference/EventSystems.EventSystem.html

Q 형변환이 안된다?

private void Start()
{
    GameObject prefab = Resources.Load<GameObject>($"Prefabs/UI/UI_OptionPopup");
    if (prefab == null)
    {
        Debug.Log($"Failed to load prefab : UI/UI_OptionPopup");
    }

    UI_OptionPopup optionPopup = Instantiate(prefab) as UI_OptionPopup;

}

Instantiate 가 반환하는 것은 GameObject이다. UI_OptionPopup은 컴포넌트이다. 그래서 형변환이 불가능하다.

Q 상속받은 멤버를 숨기고 있다고 경고를 한다?

warning CS0108: 'PlayerInputController.camera' hides inherited member 'Component.camera'. Use the new keyword if hiding was intended.

public class PlayerInputController : MonoBehaviour
{
		private Camera camera;
}

camera → _camera로 바꾼다. 이미 Component.camera가 있기 때문에 그렇다.

Sound를 BGM과 Effect로 나누어서 사용하기

/// <summary>
/// Sound를 BGM, Effect로 나누어 관리
/// Effect 오디오클립만 _audioClips Dictionary에 저장, 빠르게 꺼내쓰기
/// </summary>
public class SoundManager
{
    private AudioSource[] _audioSources = new AudioSource[(int)Define.Sound.Max];
    private Dictionary<string, AudioClip> _audioClips = new Dictionary<string, AudioClip>();

    private GameObject _soundRoot = null;
    public void Init()
    {
        if (_soundRoot == null)
        {
            _soundRoot = GameObject.Find("@SoundRoot");
            if (_soundRoot == null)
            {
                _soundRoot = new GameObject { name = "@SoundRoot" };
                UnityEngine.Object.DontDestroyOnLoad(_soundRoot);

                string[] soundTypeNames = System.Enum.GetNames(typeof(Define.Sound));
                for (int count = 0; count < soundTypeNames.Length - 1; count++)
                {
                    GameObject go = new GameObject { name = soundTypeNames[count] };
                    _audioSources[count] = go.AddComponent<AudioSource>();
                    go.transform.parent = _soundRoot.transform;
                }

                _audioSources[(int)Define.Sound.Bgm].loop = true;
            }
        }
    }

    public void Play(Define.Sound type, string path)
    {
        AudioSource audioSource = _audioSources[(int)type];

        // Sound/ 경로 추가
        if (path.Contains("Sound/") == false)
        {
            path = string.Format("Sound/{0}", path);
        }

        // BGM 재생
        if (type == Define.Sound.Bgm)
        {
            AudioClip audioClip = Resources.Load<AudioClip>(path);
            if (audioClip == null)
            {
                Debug.LogError($"AudioClip Missing : {path}");
                return;
            }

            if (audioSource.isPlaying)
            {
                audioSource.Stop();
            }

            audioSource.clip = audioClip;
            audioSource.Play();
        }

        // Effect 재생
        else if (type == Define.Sound.Effect)
        {
            AudioClip audioClip = GetAudioClip(path);
            if (audioClip == null)
            {
                return;
            }

            audioSource.PlayOneShot(audioClip);
            return;
        }
    }

    private AudioClip GetAudioClip(string path)
    {
        AudioClip audioClip = null;

        // Dictionary에 저장된 AudioClip이 있다면 반환
        if (_audioClips.TryGetValue(path, out audioClip))
        {
            return audioClip;
        }

        // Resources.Load로 AudioClip을 찾아서 Dictionary에 저장
        audioClip = Resources.Load<AudioClip>(path);
        if (audioClip == null)
        {
            Debug.LogError($"AudioClip Missing : {path}");
            return null;
        }

        _audioClips.Add(path, audioClip);
        return audioClip;
    }
}

240518_??연산자

Q 유니티 프로젝트 깃허브에 옮길 때 지워도 되는 파일?

Asset / Package / ProjectSetting 만 있으면 된다.

Library / Logs / Temp / UserSettings 지우기

Q 매개변수로 this 넘기는 의미?

public static T GetOrAddComponent<T>(this UnityObject uo) where T : Component
{
    return uo.GetComponent<T>() ?? uo.AddComponent<T>();
}

Extension Method로 기능을 추가한다. GameObject가 Sealed 클래스로 상속이 불가능하다. 그래서 ComponentHolderProtocol에서 기능을 추가한 것 같다. 여기는 열려있어서 내용을 볼 수 있었다.

Q ?? 어떤 기능이지?

public static T GetOrAddComponent<T>(this UnityObject uo) where T : Component
{
    T component = uo.GetComponent<T>();
    if (component == null)
    {
        component = uo.AddComponent<T>();
    }
    return component;
}

이 내용과 위에 내용이 같다.

240520_TimeSpan

Q 스프라이트 시트에서 잘라서 쓰려는데 경로에서 못 불러온다?

 Sprite[] sprites = Resources.LoadAll<Sprite>("Sprites/GameIcon");

 // 스프라이트 배열에서 원하는 스프라이트 찾기
 foreach (Sprite sprite in sprites)
 {
     if (sprite.name == "GameIcon_16")
     {
         SoundOff = sprite;
     }
     else if (sprite.name == "GameIcon_5")
     {
         SoundOn = sprite;
     }
 }

전체를 불러와서 잘라서 써야 한다.

Q 분, 숫자 표시 편하게 하기?

 private void GetLeftTimeText()
 {
     float leftSecond = Managers.Game.LeftSecond;
     TimeSpan timeSpan = TimeSpan.FromSeconds(leftSecond);
     LeftTimeText.text = $"{timeSpan.Minutes:D2}:{timeSpan.Seconds:D2}";
 }

 private void GetScoreText()
 {
     ScoreText.text = $"{Managers.Game.Score:N0}점";  
 }

240521_ObjectPool

ObjectPool


public class ObjectPool : MonoBehaviour
{
    // 오브젝트 풀 데이터를 정의할 데이터 모음 정의
    [System.Serializable]
    public class Pool
    {
        public string tag;
        public GameObject prefab;
        public int size;
    }

    public List<Pool> Pools;
    public Dictionary<string, List<GameObject>> poolDictionary;

    private void Awake()
    {
        poolDictionary = new Dictionary<string, List<GameObject>>();
        foreach (var pool in Pools)
        {
            List<GameObject> objectPool = new List<GameObject>();
            for (int i = 0; i < pool.size; i++)
            {
                GameObject obj = Instantiate(pool.prefab);
                obj.SetActive(false);
                objectPool.Add(obj);
            }
            poolDictionary.Add(pool.tag, objectPool);
        }
    }

    public GameObject SpawnFromPool(string tag)
    {
        if (!poolDictionary.ContainsKey(tag))
            return null;

        foreach (GameObject obj in poolDictionary[tag])
        {
            if (!obj.activeInHierarchy)
            {
                obj.SetActive(true);
                return obj;
            }
        }
        return null;
    }
}
 				// 1 List
 				foreach (var pool in Pools)
        {
            List<GameObject> objectPool = new List<GameObject>();
            for (int i = 0; i < pool.size; i++)
            {
                GameObject obj = Instantiate(pool.prefab);
                obj.SetActive(false);
                objectPool.Add(obj);
            }
            poolDictionary.Add(pool.tag, objectPool);
        }
        
        // 2 Dictionary
        foreach (GameObject obj in poolDictionary[tag])
        {
            if (!obj.activeInHierarchy)
            {
                obj.SetActive(true);
                return obj;
            }
        }

1에서는 ObjectPool GameObject를 Awake()했을 때 poolDictionary에다가 Pools에 있는 내용대로 Instantiate을 해서 메모리 할당을 해주는 내용이다. objectPoold은 리스트로 동일한 타입을 저장해 놓은 동적 배열이다. 메모리에 연속해서 붙어있다. 특징은 동적으로 메모리가 증가해 Add를 했는데 공간이 없으면 다시 필요한 메모리를 요청해서 할당받는다.

2는 poolDictionary에다가 만들어 놓은 객체를 사용할 때 쓴다. Dictionary의 특징은 해시기반으로 만들어져서 빠르게 접근할 수 있는 것이다. <string, ~ > 키를 가지고 O(1)에 찾을 수 있다. 그래서 여기서는 원하는 GameObject의 타입을 string으로 찾아서 list를 들고온다. 그리고 거기서 돌면서 안쓰고 있는 오브젝트를 찾아서 켜서 반환해준다.

List와 Dictionary 모두 O(1)에 데이터에 접근할 수 있다. 하지만 데이터가 추가될 때 방식, 데이터를 접근할 때 인덱스, 해시를 이용하는 것이 차이점이다.

Instantiate Destroy를 많이 하면 안된다. 왜냐하면 메모리를 할당 해제하면 그것 자체로도 OS에 요청해서 systemcall을 한다. 그리고 GC가 안쓰는 메모리 해제해주는데 그것도 자주 발생하면 게임이 끊긴다. 그리고 필요할 때 할당해제하면 메모리도 파편화가 된다. 그래서 한번에 커다란 덩어리로 만들어 놓아서 메모리에 미리 올려놓는 것이 자주 사용하는 오브젝트는 성능 향상에 좋다.

240522_Transition

Q animator.SetBool() 작동 원리? Transition?

  • MovingEvent에 Run을 등록했다
  • Run에서 IsRun변수를 벡터의 크기가 0.5이상이면 True로 바꾼다.
  • Animator.String(“isRun”)을 사용해서 참조한다. 애니메이션이 설정해놓은 Transition에 맞게 변한다.
public class TD : TDP
{
    private static readonly int IsRun = Animator.String("isRun");
	
    protected override void Awake()
    {
        base.Awake();
    }

    private void Start()
    {
        characterController.MovingEvent += Run;
    }

    private void Run(Vector2 vector)
    {
        animator.SetBool(IsRun, vector.magnitude > .5f);
    }
}

240523_던전스파르타 프로젝트

일주일동한 진행한 유니티 팀플이 끝났다

https://github.com/SandyLee-00/Unity_Dodge

Good : 화면공유를 하고 트러블 슈팅을 하면서 문제점을 잘 찾을 수 있었다. 기능구현이 생각보다 빠르게 되었다.

Bad : 깃으로 협업을 하면서 프리팹, 씬, 컴포넌트 변수들이 제대로 안들어가서 다시 해야하는게 반복되었다. 코드 페이지 인코딩 부분을 초반에 신경쓰지 않아서 그냥 진행했고 한글주석이 계속해서 깨졌다.

느낀점 : 소통이 잘 되어서 문제가 생겼을 때 해결이 빠르게 되었다. 디버깅 시간을 고려해서 일정을 짜야겠다.

TODO : UI, Resource Manager 만들어서 좀 더 성능과 재사용성을 높일 수 있었는데 리팩토링할 시간이 없었다.

240524_싱글턴

Q 프로젝트 만들기만 했는데 에디터 레이아웃을 못 불러온다고 한다

The layout "C:/Users/syjy8/AppData/Roaming/Unity/Editor-5.x/Preferences\Layouts\current\default-2022.dwlt" could not be fully loaded, this can happen when the layout contains EditorWindows not available in this project.
UnityEditor.WindowLayout:LoadDefaultWindowPreferences ()

Untitled

Clean Cache를 누른다

Q 싱글턴에서 객체가 어떻게 메모리에 올라가 있지?

  • Lazy Loading, 싱글톤이 처음 호출될 때 기능이 초기화된다.
  1. instance 변수 초기화 : 클래스의 static 멤버이다. 클래스가 처음 로드 될 때 메모리에 공간이 할당된다. 하지만 실제 인스턴스의 값이 할당되지 않는다.
  2. Instance 프로퍼티 접근 : 첫번째로 Instance 를 호출했을 때 instance 가 null인지 확인한다.
  3. FindObjectOfType : 현재 씬에서 T 타입 객체가 있는지 찾는다. 있으면 그 싱글턴 컴포넌트를 instance에 할당한다.
  4. new GameObject() : 씬에서 그 타입 컴포넌트가 없으면 붙여질 singletonObject 를 만들고 거기에 싱글톤 컴포넌트를 붙힌다.
  5. Awake : MonoBehaviour을 상속받아서 라이프 사이클대로 돌아간다. 컴포넌트가 붙어있는 게임 오브젝트, 그 밑에 컴포넌트가 씬이 전환될 때 안 사라지게 한다.
  • instance 가 static 변수여서 클래스의 모든 객체가 동일한 객체를 참조한다. → 싱글턴
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
    private static T instance;

    public static T Instance
    {
        get
        {
            if (instance == null)
            {
                instance = (T)FindObjectOfType(typeof(T));

                if (instance == null)
                {
                    GameObject singletonObject = new GameObject();
                    instance = singletonObject.AddComponent<T>();
                }
            }
            return _instance;
        }
    }
    
    public void Awake()
    {
				DontDestroyOnLoad(instance);        
    }
}

240527_Application.persistentDataPath

Q 주석 다 풀고 다 접는 단축키?

  • CTRL M O / CTRT M L

Q 애플리케이션의 지속적인 데이터를 저장할 수 있는 위치?

  • Application.persistentDataPath 하면 각 운영체제에 맞게 데이터를 저장할 수 있는 경로를 준다.
public string _path = Application.persistentDataPath + "/SaveData.json";

public void SaveGame()
{
	string jsonStr = JsonUtility.ToJson(Managers.Game.SaveData);
	File.WriteAllText(_path, jsonStr);
	Debug.Log($"Save Game Completed : {_path}");
}

public bool LoadGame()
{
	if (File.Exists(_path) == false)
		return false;

	string fileStr = File.ReadAllText(_path);
	GameData data = JsonUtility.FromJson<GameData>(fileStr);
	if (data != null)
	{
		Managers.Game.SaveData = data;
	}

	Debug.Log($"Save Game Loaded : {_path}");
	return true;
}

Q [ ] 문법 의미?

  • Attribute 메타 데이터를 추가한다 메타데이터는 코드 실행 시점에 해당 요소에 대한 추가 정보를 제공하며, 주로 직렬화, 특수 처리를 위해 사용한다.
  • Serializable : 직렬화 가능하다.
  • XmlRoot("ArrayOfTextData") : XML의 루트 요소 ArrayOfTextData로 직렬화/역직렬화 된다.
  • [XmlElement("TextData")] : 리스트의 각 항목이 XML의 TextData 요소에 매핑된다.
[Serializable, XmlRoot("ArrayOfTextData")]
public class TextDataLoader : ILoader<int, TextData>
{
    [XmlElement("TextData")]
    public List<TextData> _textData = new List<TextData>();

    public Dictionary<int, TextData> MakeDictionary()
    {
        Dictionary<int, TextData> dictionary = new Dictionary<int, TextData>();
        foreach(TextData textData in _textData)
        {
            dictionary.Add(textData.ID, textData);
        }
        return dictionary;
    }
}

Q 가장 인자 적은 Instantiate에서 worldPositionStays: false이 기본값이 이유?

  • 부모 Transform을 따라가고 프리팹의 worldPosion을 따라가지 않게 한다.
public static T Instantiate<T>(T original, Transform parent) where T : Object
{
    return Instantiate(original, parent, worldPositionStays: false);
}

Q worldPositionStays: false에서 : 문법의 의미?

  • named argument : 변수를 넘기는데 이름을 지정해서 넘긴다. 순서 바꿔도 되게 하고 가독성도 좋아진다.

240528_new()제약조건

Q where Loader : ILoader<Key, Item>, new()?

new()가 왜 있지?

  • 제네릭 타입 매개변수를 사용하여 객체를 생성하려면 new() 제약 조건이 필요하다. 컴파일러는 해당 제네릭 타입이 매개변수가 없는 기본 생성자를 가지고 있는 것을 보장한다.

Q using (MemoryStream stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(textAsset.text))) ?

using이 왜 붙어있지?

  • using 뒤에 코드 블록이 종료되면 자동으로 Dispose 메서드를 호출하여 리소스를 해제한다. MemoryStream 객체가 더 이상 필요하지 않게 되었을 때 자동으로 정리해줄 수 있다.
private Loader LoadXml<Loader, Key, Item>(string name) where Loader : ILoader<Key, Item>, new()
{
    XmlSerializer serializer = new XmlSerializer(typeof(Loader));
    TextAsset textAsset = Resources.Load<TextAsset>($"Data/{name}");
    if (textAsset == null)
    {
        Debug.LogError($"DataManager::LoadXml() failed. name={name}");
        return default;
    }

    using (MemoryStream stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(textAsset.text)))
    {
        return (Loader)serializer.Deserialize(stream);
    }
}

Q static BindUIEvent 함수가 abstract UI_Base에 있는게 좋을지 Extension에 있는게 좋을지?

  • UI와 관련된 클릭 등에 대한 이벤트를 바인딩해주는 역할이다. 근데 클릭 이벤트를 게임 오브젝트에 바인딩하는 역할이여서 Extension에 있는게 더 좋아보인다. 어차피 전역적으로 쓰기 위해서 static 붙혀 놓았다.
public static void BindUIEvent(this GameObject _gameObject, Action action, Define.UIEvent type = Define.UIEvent.Click)
{
    UI_EventHandler ui_EventHandler = _gameObject.GetOrAddComponent<UI_EventHandler>();

    switch (type)
    {
        case Define.UIEvent.Click:
            ui_EventHandler.OnClickHandler -= action;
            ui_EventHandler.OnClickHandler += action;
            break;
        case Define.UIEvent.Pressed:
            ui_EventHandler.OnPressedHandler -= action;
            ui_EventHandler.OnPressedHandler += action;
            break;
        case Define.UIEvent.PointerDown:
            ui_EventHandler.OnPointerDownHandler -= action;
            ui_EventHandler.OnPointerDownHandler += action;
            break;
        case Define.UIEvent.PointerUp:
            ui_EventHandler.OnPointerUpHandler -= action;
            ui_EventHandler.OnPointerUpHandler += action;
            break;
    }
}

Q popup = null 필요할까?

popup은 Destory 한 다음에 여기서 안쓰인다. 그리고 Destroy로 해당 GameObject 지우면 플레그 설정하고 프레임 끝날 때 한번에 지운다. 붙어있던 Component도 같이 사라져서 UI_Popup 타입으로 붙어있던 popup도 사라진다. 굳이 필요할까?

  • 내가 나중에 짤 때 Destory로 지웠는데 실수로 popup 사용할 수 있다. 지워진 오브젝트 코드 뒤에서 사용 하지 말라고 다른 사람을 위해 표시한다.
public void ClosePopupUI(UI_Popup popup)
{
    if (popupStack.Count == 0)
    {
        Debug.LogError("UIManager::ClosePopupUI() popupStack is empty.");
        return;
    }

    if(popupStack.Peek() != popup)
    {
        Debug.LogError("UIManager::ClosePopupUI() popupStack.Peek() != popup.");
        return;
    }
    
    GameObject.Destroy(popupStack.Pop().gameObject);
    popup = null;
    order--;
}

240529_TextMeshProUGUI

Q 프로젝트 다 만든 다음에 이름 바꾸기

  • unity Hub에서 지우기, 저장된 폴더 이름 바꾸기, .sln, .csproj 파일 지우기,

Q 메인카메라 씬에서 보는 각도로 설정하기

  • 메인카메라 선택 → Ctrl + Shift + F

Q GameObject인 HPBar는 바인딩이 되는데 Text는 안된다.

Text가 아니라 타입이 TextMeshProUGUI이다.

protected void BindText(Type type) { Bind<TextMeshProUGUI>(type); }

이거 사용하는걸로 바꿨다.

240530_InputSystemControlScheme

Q InputSystem에서 보내는 메시지가 PlayerInputController에 전달이 안된다?

PlayerInputController의 OnMove / OnLook / OnJump Debug.log를 해도 호출이 안된다.

  • Control Scheme에 Keybord / Mouse 추가를 안했다.

240531_DontDestroyOnLoad

Q DontDestoryOnLoad 적용 범위?

  • DontDestroyOnLoad는 게임 오브젝트 단위로 적용되므로, instance가 부착된 게임 오브젝트와 그 자식 및 모든 부착된 컴포넌트가 씬 전환 시에도 파괴되지 않고 그대로 유지한다.
  • 부모는 냅두고 본인 게임오브젝트, 본인 컴포넌트, 자식게임오브젝트, 자식 컴포넌트만 들고간다.
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
    private static T instance;

    public static T Instance
    {
        get
        {
            if (instance == null)
            {
                instance = (T)FindObjectOfType(typeof(T));

                if (instance == null)
                {
                    GameObject singletonObject = new GameObject();
                    instance = singletonObject.AddComponent<T>();
                }
            }
            return _instance;
        }
    }
    
    public void Awake()
    {
				DontDestroyOnLoad(instance);        
    }
}

0개의 댓글