한 주 회고

이번 주에는 지난 주 새로운 InputSystem을 사용해 게더 타운을 유니티로 만들어 보는 시간을 가졌다.

https://github.com/JeongJuChan/SpartaTown

 

GitHub - JeongJuChan/SpartaTown

Contribute to JeongJuChan/SpartaTown development by creating an account on GitHub.

github.com

Input System에서 SendMessage와 Invoke Unity Event의 차이를 알고 사용해 볼 수 있었다.

또한 UI 바인딩을 적용하는 방법과 업스케일링이라는 작업도 경험해보았다.

 

다음 주 목표

 다음 주는 벽돌깨기 팀 프로젝트를 완성하게 될텐데 계획했던 것을 모두 구현할 수 있었으면 좋겠다. 지난 주에 구현했던 것과 구현할 것 그리고 다른 팀원과 겹치는 부분에서 받아올 것 다 정리해놨으니 금방 작업만 하면 된다. 다음 주도 파이팅!

오늘의 학습 키워드

Visual Studio 자동완성, Awake vs OnEnable

 

공부한 내용

Visual Studio 자동완성

Edit - Preferences - External Script Editor의 항목이 internal로 뜨며 자동완성이 안될 때가 있다.

Browse를 눌러 Visual Studio 경로로 지정을 해보았으나 실패했다.

이번 경우는 Visual Studio Editor가 설치가 되어있지 않아서 생기는 문제였다.

패키지 설치 후 스크립트를 껐다 켰더니 되는 모습!

 

 

Awake vs OnEnable

UI매니저는 Awake에서 초기화하는 싱글턴이고 다른 스크립트는 OnEnable에서 이벤트 등록을 했는데 UI 매니저의 싱글턴 인스턴스가 null이라는 오류가 나왔다.

왜 Awake에서 초기화를 진행했는데 OnEnable에서 null일까 이상했다.

그래서 확인을 해보려고 싱글턴의 Awake와 다른 스크립트의 OnEnable에 중단점을 찍어봤더니 다른 스크립트의 OnEnable이 먼저 불린 경우였다.

이유를 알기 위해 검색을 좀 해봤더니 OnEnable동일한 스크립트에서만 Awake 뒤에 동작하는 것을 보장하고 다른 것은 그렇지 않다는 것이었다. Execution Order를 이용하여 호출 순서를 지정하거나 커스텀 메서드를 생성하여 싱글턴의 Awake가 불렀는지 체크하고 그 이후에 동작하도록 만들 수 있지만 Execution Order는 다른 스크립트도 꼬일 수 있어서 하지 않았고 커스텀 메서드는 지금 방식에서 그냥 Start를 이용하는 게 간편하다고 생각해서 Start로 옮겨 해결하게 되었다.

해결된 모습

 

 

오늘의 회고

 오늘은 팀 과제 발제가 있는 날이었다. 팀원들과 고전게임을 만들어 보는 과제였는데 우리는 닷지, 똥피하기, 벽돌깨기 중 벽돌깨기가 가장 안 해본 프로젝트에 가까워서 선택하기로 했다. 와이어 프레임도 그리고 필수 구현 기능과 추가 구현 기능으로 나눠서 필수 구현부터 팀원들과 분배하여 구현하였다. 오늘 필수 구현은 다 진행하였고 겹치는 부분을 제외하고서는 머지까지 완료했다. 협업이란 게 잘 되고 시너지가 잘 나서 능률이 올라가는 것 같아서 좋았고 재밌었다.

 이렇게만 계속 구현을 한다면 기획을 했던 것들을 다 구현할 수 있을 것 같아서 기대가 된다. 다음 주도 파이팅!

 

 

참고 :

https://forum.unity.com/threads/execution-order-of-scripts-awake-and-onenable.763688/

 

Execution Order of Scripts - Awake and OnEnable

I am facing issues related to scripts execution order and this point is out of my mind though I am an experienced, Unity developer. So I expect a...

forum.unity.com

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

 

Unity - Scripting API: MonoBehaviour.Awake()

Awake is called either when an active GameObject that contains the script is initialized when a Scene loads, or when a previously inactive GameObject is set to active, or after a GameObject created with Object.Instantiate is initialized. Use Awake to initi

docs.unity3d.com

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

 

Unity - Scripting API: MonoBehaviour.OnEnable()

Success! Thank you for helping us improve the quality of Unity Documentation. Although we cannot accept all submissions, we do read each suggested change from our users and will make updates where applicable. Close

docs.unity3d.com

 

오늘의 학습 키워드

UpScale Sampling

 

공부한 내용

UpScale Sampling

업 스케일링 샘플링 : UI는 원래 해상도로 하고 3D 씬만 낮은 해상도로 렌더링 하는 방법

- 해상도를 줄이면 픽셀이 도드라져 게임 유저가 저해상도임을 알아채기 쉬워서 나온 보완 방법

 

구현 방법

1. 카메라 2대가 필요하다(UI 카메라와 메인 카메라)

1-1 CullingMask 옵션에서 UI카메라는 UI만 찍게하고 메인 카메라는 UI를 제외한 부분을 찍게한다.

1-2 또한 UI카메라는 추가로 Clear Flags를 Depth Only로 해 두 대의 카메라가 동시에 렌더링 될 수 있도록 한다.

2. 렌더 텍스처를 담기 위한 캔버스하위의 RawImage를 하나 만든다.

3. 이후 업스케일링을 하기 위한 RenderTexture를 만들고 Raw이미지에 담는 작업을 구현한다.

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

public class RenderTextureCreater : MonoBehaviour
{
    [SerializeField] private GameObject rtCanvas;

    public void CreateRT(float ratio)
    {
        int x = (int)(Screen.width * ratio);
        int y = (int)(Screen.height * ratio);

        var rt = new RenderTexture(x, y, 24, UnityEngine.Experimental.Rendering.DefaultFormat.HDR);
        rt.Create();

        Camera.main.targetTexture = rt;

        // RawImage는 Texture에 접근 가능
        RawImage raw = Instantiate(rtCanvas).GetComponentInChildren<RawImage>();
        raw.texture = rt;
    }
}

4. 마지막으로 버튼에 연결하여 동작시킨다.

4-1. 중요한 점은 업스케일링할 Canvas(만들어놓은 프리펩)의 Sort Order버튼 UI의 Sort Order보다 높아야한다. => 그래야 보임

 

적용된 모습 (적용 전 -> 적용 후)

변경 후

 

 

오늘의 회고

 오늘은 개인 과제 제출이 있는 날이었다. 오전 중에는 구상은 해놓았지만 구현하지 못했던 것들을 구현하고 버그 잡고 Readme 쓰느라 정신이 없었던 것 같다. 오후에는 튜터님의 UI 강의를 듣고 Zep으로 레크레이션 시간을 가졌는데 꽤 재밌었다. 이후에 남은 시간에 업스케일링 부분이 있길래 공부해봤는데 나중에 최적화를 위해서 써먹어 봐야겠다.

 내일은 유니티 팀 프로젝트 과제 발제가 있는 날이다. 오전 중에 기획을 진행하고 구상하는 방식으로 팀원과 잘 의견을 공유해봐야겠다. 내일도 파이팅!

 

 

참고 :

https://github.com/ozlael/UpsamplingRenderingDemo

 

GitHub - ozlael/UpsamplingRenderingDemo: Upsampling Rendering is old school but common trick for low end devices.

Upsampling Rendering is old school but common trick for low end devices. - GitHub - ozlael/UpsamplingRenderingDemo: Upsampling Rendering is old school but common trick for low end devices.

github.com

 

오늘의 학습 키워드

바인딩, Enum.GetNames()

 

공부한 내용

UI 바인딩

UI 부분 관련해서 UI 바인딩에 관한 정보를 찾게 되었다.

UI 작업할 때 항상 문제점이 변수가 너무 많이 늘어나서 보기에 불편하다는 단점이 있었는데 UI 바인딩이 하나의 대체제가 될 수 있다고 생각이 들었다.

UI 바인딩 순서는 이렇다.

1. 바인딩의 매개변수로 들어가는 enum에는 해당 컴포넌트의 하위의 이름을 넣어둔 뒤

2. UI바인딩 메서드가 들어가 있는 UIBase를 상속 받아 enum을 키 값으로 하고 Object를 value 값으로 하는 딕셔너리에 등록을 한다.

3. Enum.GetNames()로 해당 enum 값을 배열로 불러와 foreach문을 돌며 이름으로 오브젝트를 찾아내 딕셔너리 값에 등록해준다.

4. 나중에 찾아서 쓸 때는 Get<타입>(인덱스)으로 딕셔너리 값인 Object 배열에서 타입을 가져올 수 있다. (enum인덱스 : 0,1,2... 딕셔너리 배열 인덱스 : 0,1,2...가 일치하므로)

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

public abstract class UIBase : MonoBehaviour
{
    // GameObejct가 Object를 상속받고 다른 컴포넌트들이 Object를 상속 받으므로 Object 타입으로 통일
    // UIBase를 상속받으면 _objectDict를 하나씩 가지고 있음
    private Dictionary<Type, UnityEngine.Object[]> _objectDict = new Dictionary<Type, UnityEngine.Object[]>();

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

        _objectDict.Add(typeof(T), objects);

        for (int i = 0; i < names.Length; i++)
        {
            if (typeof(T) == typeof(GameObject))
            {
                objects[i] = Util.FindGameObject(gameObject, names[i], true);
            }
            else
            {
                objects[i] = Util.FindComponent<T>(gameObject, names[i], true);
            }

            if (objects[i] == null)
            {
                Debug.LogError($"Failed To Bind {names[i]}");
            }
        }
    }

    // enum값 0,1,2,... 배열 index 값 0,1,2,...
    protected T Get<T>(int index) where T : UnityEngine.Object
    {
        UnityEngine.Object[] objects = null;
        if (!_objectDict.TryGetValue(typeof(T), out objects))
        {
            return null;
        }
        else
        {
            return (T)objects[index];
        }
    }
}
using TMPro;

public class NameUI : UIBase
{
    enum Texts
    {
        NameText,
    }

    void Start()
    {
        Bind<TMP_Text>(typeof(Texts));
    }

    public void SetName(string name)
    {
        Get<TMP_Text>((int)Texts.NameText).text = name;
    }

}

잘 불러와지는 것을 볼 수 있다.

 

오늘의 회고

  오늘은 UI 바인딩에 대한 작업을 진행했다. 거의 접해보지 못했던 작업이라 이해하는데 시간도 걸리고 다른 작업들을 많이 진행하지 못했다. 아쉽지만 내일 남은 작업이라도 열심히 해봐야겠다는 생각이 들었다. 그럼에도 불구하고 이번에 배운 것들은 굉장히 프로그래머스러운(?) 작업이라 꽤나 보람이 있었다고 느꼈다.

 내일은 과제 제출일이다. 12시까지 하던 작업을 끝내고 Readme까지 완료해야겠다. 그 다음엔 여러 사람과 친해지는 시간이 있다는데 조금 떨리고 설레기도 한 것 같다. 내일 과제 마무리 잘 해보자. 파이팅!

오늘의 학습 키워드

InputSystem(SendMessage, Invoke Unity Events)

 

공부한 내용

SendMessage vs Invoke Unity Events

SendMessage 특징

- InputAction으로 지정한 Action들의 이름 앞On이 붙은 함수를 호출하며 ex) OnMove

- InputValue를 매개변수로 받는다. (코드에서 누른 시점, 누르고 있는 시점, 뗀 시점 제어 불가)

void OnMove(InputValue value)
{
    Vector2 arrowInput = value.Get<Vector2>();
    Debug.Log(arrowInput);
    CallMoveAction(arrowInput);
}

void OnLook(InputValue value)
{
    Vector2 mousePos = value.Get<Vector2>();
    Vector2 mousePosInWorld = _mainCam.ScreenToWorldPoint(new Vector3(mousePos.x, mousePos.y, transform.position.z));

    
    Vector2 direction = (mousePosInWorld - (Vector2)transform.position).normalized;

    CallLookAction(direction);
}

void OnAttack(InputValue value)
{
    IsAttacking = value.isPressed;
}

 

Invoke Unity Events 특징

- 함수명에 상관 없이 Unity Event인스펙터에서 등록이 가능하고

- InputAction.CallbackContext매개변수로 받아 콜백 시점을 정할 수 있다. ex) performed, canceled

private void Start()
{
    var inputTypes = (InputType[])Enum.GetValues(typeof(AnimState));

    foreach (var inputAction in _playerInput.actions)
    {
        InputType state = Array.Find(inputTypes, x => x.ToString() == inputAction.name);

        switch (state)
        {
            case InputType.Move:
                inputAction.performed += OnMove;
                inputAction.canceled += OnMove;
                break;
            case InputType.Look:
                inputAction.performed += OnLook;
                break;
            case InputType.Attack:
                inputAction.performed += OnAttack;
                break;
        }
    }
}

void OnMove(InputAction.CallbackContext context)
{
    Vector2 arrowInput = context.ReadValue<Vector2>();
    Debug.Log(arrowInput);
    CallMoveAction(arrowInput);
}

void OnLook(InputAction.CallbackContext context)
{
    Vector2 mousePos = context.ReadValue<Vector2>();
    Vector2 mousePosInWorld = _mainCam.ScreenToWorldPoint(new Vector3(mousePos.x, mousePos.y, transform.position.z));

    // atan을 구하기 위해 normalized
    Vector2 direction = (mousePosInWorld - (Vector2)transform.position).normalized;

    CallLookAction(direction);
}

void OnAttack(InputAction.CallbackContext context)
{
    IsAttacking = context.performed;
}

 

애니메이션 루프 탈출

애니메이션 Attack 루프를 탈출 못하는 버그에 걸렸다.

한 번 클릭해도 다른 입력(이동)이 들어오면 Attack이 멈추지 않았다.

해결 방안으로 코루틴을 돌아 애니메이션이 끝나면 이동 속도에 따라 해당 애니메이션으로 빠져나가게 만들었다.

private void SetState(AnimState state)
{
    _state = state;

    switch (_state)
    {
        case AnimState.Idle:
            _animator.CrossFade(PlayerManager.Instance.AnimToHash.ANIM_IDLE, 0.3f);
            break;
        case AnimState.Walk:
            _animator.CrossFade(PlayerManager.Instance.AnimToHash.ANIM_WALK, 0.3f);
            break;
        case AnimState.Preslide:
            _animator.CrossFade(PlayerManager.Instance.AnimToHash.ANIM_PRESLIDE, 0.3f);
            break;
        case AnimState.Attack:
            _animator.Play(PlayerManager.Instance.AnimToHash.ANIM_ATTACK);
            StartCoroutine(CoWaitForCurrentAnimation(PlayerManager.Instance.AnimToHash.ANIM_ATTACK));
            break;
        case AnimState.Jump:
            _animator.Play(PlayerManager.Instance.AnimToHash.ANIM_JUMP);
            StartCoroutine(CoWaitForCurrentAnimation(PlayerManager.Instance.AnimToHash.ANIM_JUMP));
            break;
    }
}

private IEnumerator CoWaitForCurrentAnimation(int hashValue)
{
    bool isAnimationProgress = true;
    
    // 애니메이션이 Attack인지 확인 이후
    // 해당 애니메이션 현재 시간을 normalize 한 값이 0.9보다 클 때 탈출
    while (isAnimationProgress)
    {
        if (_animator.GetCurrentAnimatorStateInfo(0).shortNameHash == hashValue)
        {
            isAnimationProgress = _animator.GetCurrentAnimatorStateInfo(0).normalizedTime < 0.9f;
        }
        
        yield return null;
    }
    
    if (PlayerManager.Instance.Velocity.sqrMagnitude > 0)
    {
        SetState(AnimState.Walk);
    }
    else
    {
        SetState(AnimState.Idle);
    }
}

 

ReadOnly 초기화

readonly 초기 Setter생성자에서는 초기화가 된다.

- Setter는 선언할 때 같이 쓰는 것 됨

using System;
using UnityEngine;

public class AnimNameHash
{
    private const string ANIM_FRONT = "penguin_";

    [Header("AnimToHashValue")]
    public readonly int ANIM_IDLE;
    public readonly int ANIM_WALK;
    public readonly int ANIM_ATTACK;
    public readonly int ANIM_PRESLIDE;
    public readonly int ANIM_JUMP;

    public AnimNameHash()
    {
        var states = Enum.GetValues(typeof(AnimState));
        foreach (var state in states)
        {
            string stateName = ANIM_FRONT + state.ToString();

            switch (state)
            {
                case AnimState.Idle:
                    ANIM_IDLE = Animator.StringToHash(stateName);
                    break;
                case AnimState.Walk:
                    ANIM_WALK = Animator.StringToHash(stateName);
                    break;
                case AnimState.Preslide:
                    ANIM_PRESLIDE = Animator.StringToHash(stateName);
                    break;
                case AnimState.Attack:
                    ANIM_ATTACK = Animator.StringToHash(stateName);
                    break;
                case AnimState.Jump:
                    ANIM_JUMP = Animator.StringToHash(stateName);
                    break;
            }
        }
    }
}

 

오늘의 회고

 오늘은 유니티의 Input System에 대해 알아봤다. 강의에서 배운 SendMessage는 채팅 기능과 같이 바로 등록시켜줘야 하는 경우에 쓰면 좋고 Invoke Unity Events는 서버에서 다른 경로로 오는 데이터를 모두 받아야 실행할 수 있는 작업이 있을 때 사용하면 좋다고 한다. 나의 경우는 강의에서 이미 SendMessage를 사용해봤으니 이번엔 호출 시점을 정해줄 수 있는 Invoke Unity Events를 사용하기로 했다.

 내일도 오늘 작업하던 개인 과제를 더 진행할 생각이다. 내일 거의 완성을 해야 목요일까지 제출이 가능할 것 같아 열심히 해봐야겠다. 내일도 파이팅!

 

참고 :

https://docs.unity3d.com/Packages/com.unity.inputsystem@1.8/api/UnityEngine.InputSystem.html

 

Namespace UnityEngine.InputSystem | Input System | 1.8.0-pre.1

Namespace UnityEngine.InputSystem Classes Input device representing an accelerometer sensor. Input device representing the ambient air temperature measured by the device playing the content. Input device representing an attitude sensor. A collection of com

docs.unity3d.com

https://www.reddit.com/r/Unity3D/comments/hk4r5u/new_input_system_player_input_component_and_send/

 

From the Unity3D community on Reddit

Explore this post and more from the Unity3D community

www.reddit.com

 

오늘의 학습 키워드

Atan2, Tilemap

 

공부한 내용

Pixel Per Unit

Pixel Per Unit한 유닛 당 몇 픽셀을 표현할건지이다.

아래의 사진에서 보자면 100픽셀 당 한 유닛을 표현하는데 가로 기준으로 16픽셀만 들어가 있으니 16/100 유닛을 표현할 수 있는 것이다. 만약 Pixel Per Unit을 16으로 한다면 해당 텍스처가 가로로 한 칸을 표현할 수 있게 된다.

그러므로 Pixel Per Unit 값이 작을 수록 크기가 커지고 값이 클수록 크기가 작아진다.

 

TileMap

타일맵은 아래 사진 처럼 그릴 때 레이어를 나눠서 그릴 수 있고 충돌도 레이어를 나눠서 적용할 수 있다.

추가로 타일맵을 켜면 Focus On 모드로 필터링을 할 수 있다.

 

Atan2

아래의 코드처럼 MathF.Atan2를 사용한다면 벡터의 각도를 구할 수 있다.

그 각도의 절댓값이 90보다 크다면(-90~-180 || 90~180) Sprite의 방향을 바꾼다는 의미이다.

private void Look(Vector2 direction)
{
    float angle = MathF.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;

    bool flipX = Mathf.Abs(angle) > 90f;
    _renderer.flipX = flipX;
}

해당 내용을 잘 설명한 블로그가 있다. 나는 개인적으로 이해하는데 도움이 많이 됐다.

https://robotree.tistory.com/7

 

[Unity3D] 게임 오브젝트의 마우스 방향으로 바라보기

슈팅게임 등 마우스 포인터의 방향으로 게임 오브젝트가 회전을 해야 할 경우가 있습니다. 본인의 경우에는 간단하게 LookAt(); 이라는 함수를 써볼 생각했었으나, 약간의 오류를 발견하고 좀 더

robotree.tistory.com

 

 

오늘의 회고

 오늘은 유니티 입문에 대한 강의를 듣고 개인 과제를 조금 진행해보았다. 강의를 듣거나 개인 과제를 진행하면서 잊어버렸던 부분과 새롭게 알게된 부분들이 계속 생긴다. 새로 알게된 부분은 실제로 적용해보면서 익히고 잊어버렸던 부분은 다음에 또 까먹지 않게 내 방식대로 정리해야겠다.

 내일부터 본격적으로 개인 과제를 수행하게 될텐데 이미 어떻게 만들지는 대략적으로 생각해놨으니 구현하기만 하면 될 것 같다. 내일도 파이팅!

한 주 회고

이번 주에는 지난 주 개인 TextRPG를 개발한 것을 기반으로 팀 단위로 개발하게 되었다.

이번 주에는 특히나 Json에서 헤매는 부분이 많았는데 다음에 또 이러한 문제가 나오면 금방 해결할 수 있을 것 같다.

다른 팀원과의 협업 중 버그가 생긴 것도 차근차근 보면서 해결할 수 있었고 좋은 경험이었다고 생각한다.

 

 

다음 주 목표

다음 주는 다시 유니티로 돌아가서 개인 과제를 진행하게 된다. 잠깐 살펴봤을 때는 수학적인 개념과 이벤트를 많이 사용하는 것 같던데 굉장히 도움이 많이 되지 않을까 하는 생각이 들었다. 다음 주도 파이팅!

오늘의 학습 키워드

참조

 

공부한 내용

중복 장착 버그

팀 과제를 제출하고 나서 문제를 발견했는데 같은 아이템이 여러 개라면 하나만 장착해도 다 장착되는 문제가 있었다.

디버그를 해보니 문제는 몬스터의 보상 아이템의 참조 문제라는 것을 알게 되었다.

몬스터 생성자의 마지막 매개변수로 보상 아이템이 들어가는데 같은 아이템을 참조해버리니 보상을 줄 때도 같은 메모리를 참조하여 같은 아이템이라고 생각하고 값이 같이 변경 되었던 것이다.

monsters = new Monster[]
{
    new Monster("Lv.1 미니언", 1, 10, 3, 10,  Items[8]),
    new Monster("Lv.2 미니언", 2, 15, 5, 20, Items[9]),
    new Monster("Lv.5 대포미니언", 5, 25, 8, 50, Items[8]),
    new Monster("Lv.3 공허충", 3, 10, 9, 30, Items[9])
};

그래서 이를 해결하기 위해 아이템 데이터(원본)으로 새 아이템을 만든 뒤 보상 아이템으로 넣어주는 작업을 진행했다.

Item dropItemData = null;
switch (player.Job)
{
    case "전사":
        dropItemData = GetDropEquipmentWithID(itemData, 1005);
        break;
    case "궁수":
        dropItemData = GetDropEquipmentWithID(itemData, 1009);
        break;
    case "마법사":
        dropItemData = GetDropEquipmentWithID(itemData, 1013);
        break;
    case "도적":
        dropItemData = GetDropEquipmentWithID(itemData, 1017);
        break;
}

Item dropItemDefault = SetItemData(Items[0]);
Item dropItemOfJob = SetItemData(dropItemData);

monsters = new Monster[]
{
    new Monster("Lv.1 미니언", 1, 10, 3, 10, dropItemDefault),
    new Monster("Lv.2 미니언", 2, 15, 5, 20, dropItemOfJob),
    new Monster("Lv.5 대포미니언", 5, 25, 8, 50, Items[ItemCount - 2]),
    new Monster("Lv.3 공허충", 3, 10, 9, 30, Items[ItemCount - 1])
};

해결된 모습

 

 

오늘의 회고

 오늘은 팀 과제 제출이 있었다. 고민도 많이 한 만큼 배운 것도 많았고 조금 아쉬웠던 부분은 조금 더 많이 만들지 못해 아쉬웠던 것 같다. 그놈의 json 데이터가 뭔지 이번 주에 날 괴롭혔지만 오히려 지금 배우는 게 더 좋을 거라고 생각한다. 그리고 참조에 관해서도 많이 배워서 다음엔 로직 만들 때 고려하고 만들어야 겠다.

 다음 주에는 유니티 강의를 듣는 시간과 개인 과제가 주어지는데 다시 유니티를 한다니 기대가 되고 재밌을 것 같다. c#에서 많이 단단해졌으니 유니티로 넘어가서 배웠던 것들을 잘 써먹어 봐야겠다. 다음 주도 파이팅!

오늘의 학습 키워드

사용자 데이터와 데이터 테이블 나누기

 

공부한 내용

아이템 클래스 역할 나누기

아래의 상황에서 아이템 클래스 배열을 Json화 하여 변경되는 사용자 데이터도 반영시키는 것이 목표였다.

namespace TextRPG_Team
{
    public enum ItemType
    {
        Equipment,
        Consumable,
    }

    public class Item
    {
        public int Id { get; set; }
        public string Name { get; }
        public string Description { get; }
        public ItemType Type { get; }
        public bool IsHave { get; set; }
        public int BuyGold { get; }
        public int SellGold { get; }

        public class Equipment
        {
            public int Atk { get; }
            public int Def { get; }
            public bool IsEquiped { get; set; }

            public Equipment(int atk, int def, bool isEquiped)
            {
                Atk = atk;
                Def = def;
                IsEquiped = isEquiped;
            }
        }

        public class Consumable
        {
            public int Count { get; set; }
            public int Amount { get; }

            public Consumable(int count, int amount)
            {
                Count = count;
                Amount = amount;
            }
        }

        public Equipment EquipmentData { get; set; }
        public Consumable ConsumableData { get; set; }

        public Item(int id, string name, string description, ItemType type, bool isHave, int buyGold, int sellGold)
        {
            Id = id;
            Name = name;
            Description = description;
            Type = type;
            IsHave = isHave;
            BuyGold = buyGold;
            SellGold = sellGold;
        }
    }
}

 

하지만 Item 배열에 다운캐스팅 한다고 해서 서브 클래스인 Equipment와 Consumable의 데이터가 Item으로 담길리는 없다.

그래서 구조를 바꾸기로 결정했다.

첫 번째 아이디어 : Item을 상속받는 Equipment와 Consumable이 있고 이미 Item배열인 Items가 있는 현재 구조에서 Equipment 배열과 Consumable 배열을 만들어 json으로 따로 관리하려 했으나 메모리 낭비라고 생각이 듬

두 번째 아이디어 : 그래서 둘의 데이터는 같이 관리하되 장착했는지 여부와 몇 개인지는 Item에 통합하여 관리하도록 함 (사용자 데이터 관련한 부분들은 따로 테이블을 만들어 세팅하는 방식)

핵심은 아이템 테이블사용자 데이터분리하는 것이었다. - 추후 세팅

타입으로 판단하여 Item 안의 해당 타입일때만 이너 클래스를 생성하도록 하였다.

아이템 테이블
유저 데이터

namespace TextRPG_Team
{
    public enum ItemType
    {
        Equipment,
        Consumable,
    }

    public class Item
    {
        public int Id { get; set; }
        public string Name { get; }
        public string Description { get; }
        public ItemType Type { get; }
        public bool IsHave { get; set; }
        public int BuyGold { get; }
        public int SellGold { get; }

        public class Equipment
        {
            public int Atk { get; }
            public int Def { get; }
            public bool IsEquiped { get; set; }

            public Equipment(int atk, int def, bool isEquiped)
            {
                Atk = atk;
                Def = def;
                IsEquiped = isEquiped;
            }
        }

        public class Consumable
        {
            public int Count { get; set; }
            public int Amount { get; }

            public Consumable(int count, int amount)
            {
                Count = count;
                Amount = amount;
            }
        }

        public Equipment EquipmentData { get; set; }
        public Consumable ConsumableData { get; set; }

        public Item(int id, string name, string description, ItemType type, bool isHave, int buyGold, int sellGold)
        {
            Id = id;
            Name = name;
            Description = description;
            Type = type;
            IsHave = isHave;
            BuyGold = buyGold;
            SellGold = sellGold;
        }
    }
}

 

 

오늘의 회고

 오늘은 아이템 사용자 데이터 세팅과 관련해서 작업을 진행했다. 몇 시간 동안 머리를 부여 잡고 고민을 했는데 해결 방법이 잘 떠오르지 않아 힘들었던 것 같다. 하지만 방법을 알게되고 가슴이 막막한 기분을 떨쳐내고 후련하다고 느꼈다. 어려운 과제였다고 느꼈지만 알고 나니까 아무것도 아니었다는 생각도 들고 후련하기도 하다. 이를 동기부여 삼아서 더 열심히 할 수 있을 것 같다.

 내일은 이번에 진행한 텍스트 RPG의 제출일이다. 12시에 바로 제출해야 하니 내일은 주석 정리, 버그 체크, Readme 작성에 집중하도록 해야겠다. 과제 잘 마무리 해보자. 내일도 파이팅!

 

 

참고 : 

https://askforyou.tistory.com/58

 

[c#] JSON.NET 을 이용한 Json 데이터 파싱 - Json Key Name 가져오기 (JObject, JToken)

[c#] JSON 형변환(Json Convert) with JSON.NET [c#] JSON 형변환(Json Convert) with JSON.NET 어떤 언어든 코딩을 하면서 많이 사용하는것 중의 하나가 JSON 입니다. C# 에서는 JSON 을 사용할때 가장 많이 이용하는 것

askforyou.tistory.com

 

오늘의 학습 키워드

System.InvalidCastException

 

공부한 내용

System.InvalidCastException 오류

Skill 클래스의 필드인 SkillType에 따라 서브 클래스들을 다운 캐스팅하는 작업을 진행하려는데 System.InvalidCastException 오류가 떴다.

그래서 무엇이 문제인지 확인하기 위해 아래에 보이듯이 테스트용 Skill 리스트를 새로 만들고 데이터를 넣은 후 다운캐스팅을 해봤다.

하지만 이 로직에서는 문제가 없었는데 아래의 multiSkill과 비교했더니 테스트용 Skill은 Action 값이 들어있었고 내가 json데이터로 받아온 Skill은 Action 값이 들어있지 않았다.

그래서 이유를 짐작해보면

1. Json 데이터에 담긴 업캐스팅을 한 서브클래스들은 베이스 클래스인 Skill의 정보만 저장할 수 있고

2. 서브 클래스에만 있는 Action은 저장되지 않는다.

3. 그래서 업캐스팅을 진행하더라도 Json 데이터로 불린 Skill은 서브 클래스에 대한 정보가 없는 것이다.

if (skillNum != 0)
{

    switch (player.Skills[skillNum - 1].Type)
    {
        case SkillType.SigleTarget:
            SingleSkill singleSkill = (SingleSkill)player.Skills[skillNum - 1];
            singleSkill.UseSkill(monsters[targetIndex], damage);
            Console.WriteLine($"{monsters[targetIndex].Name}을(를) 맞췄습니다. {damageMessage}");
            break;
        case SkillType.MultipleTarget:
            List<Skill> skills = new List<Skill>();
            skills.Add(new SingleSkill("라이징 샷", "공격력 * 2.25 로 하나의 적을 공격합니다.", 15, 2.25f, null));
            SingleSkill single = (SingleSkill)skills[0];
            MultipleSkill multiSkill = (MultipleSkill)player.Skills[skillNum - 1];

            // 출력을 스킬에서 처리
            multiSkill.UseSkill(monsters, damageMessage, damage);
            break;
    }
namespace TextRPG_Team
{
    public enum SkillType
    {
        SigleTarget,
        MultipleTarget
    }

    public class SingleSkill : Skill
    {
        public Action<Monster, float, float> SingleAction { get; }

        public SingleSkill(string name, string description, int cost, float damageMod, Action<Monster, float, float> singleAction) 
            : base(name, description, SkillType.SigleTarget, cost, damageMod)
        {
            SingleAction = singleAction;
        }

        public void UseSkill(Monster target, int damage)
        {
            SingleAction?.Invoke(target, damage, DamageMod);
        }
    }

    public class MultipleSkill : Skill
    {
        public int TargetCount { get; }
        public Action<Monster[], string, float, float, int> MultipleAction { get; }

        public MultipleSkill(string name, string description, int cost, float damageMod, Action<Monster[], string, float, float, int> multipleAction, int targetCount = int.MaxValue) 
            : base(name, description, SkillType.MultipleTarget, cost, damageMod)
        {
            TargetCount = targetCount;
            MultipleAction = multipleAction;
        }

        public void UseSkill(Monster[] targets, string damageMessage, int damage)
        {
            MultipleAction?.Invoke(targets, damageMessage, damage, DamageMod, TargetCount);
        }
    }

    public class Skill
    {
        public string Name { get; }
        public SkillType Type { get; }
        public int Cost { get; }
        public float DamageMod { get; }
        public string Description { get; }

        // 단일 타겟 스킬과 다중 타겟 스킬을 생성자로 나눴다.
        
        // 배틀 시스템 들어갔을 때 type에 따라 캐스팅하여 UseSkill 호출하면 됨

        public Skill(string name, string description, SkillType type, int cost, float damageMod)
        {
            Name = name;
            Cost = cost;
            Type = type;
            DamageMod = damageMod;
            Description = description;
        }

    }
}

그래서 아래의 방식으로 해결했다.

그 방식은 서브 클래스에 스킬 메서드를 저장하지 않고 정보만 저장한 뒤 나중에 타입에 따라서 불러오는 것이다. (런타임 중 초기화)

구현해 볼 만한 또 다른 방안으로는 데이터로 인덱스 정보만 저장한 뒤 인덱스로 메서드를 불러오는 방식도 좋을 것 같다.

public class Skill
{
    public string Name { get; }
    public string Description { get; }
    public SkillType Type { get; }
    public int Cost { get; }
    public float DamageMod { get; }
    public int TargetCount { get; }

    // 단일 타겟 스킬과 다중 타겟 스킬을 생성자로 나눴다.
    
    // 배틀 시스템 들어갔을 때 type에 따라 캐스팅하여 UseSkill 호출하면 됨

    // targetCount == 0 은 전체 공격으로 설정
    public Skill(string name, string description, SkillType type, int cost, float damageMod, int targetCount = 1)
    {
        Name = name;
        Cost = cost;
        Type = type;
        DamageMod = damageMod;
        Description = description;
        TargetCount = targetCount;
    }
}

 

오늘의 회고

 오늘은 캐릭터 스킬 적용과 인벤토리, 치명타, 회피 UI 작업을 진행했다. 스킬을 적용하는 부분에 있어서 애를 좀 먹었지만 그래도 어떻게 잘 해결해 나간 것 같다. 다른 팀원과의 작업 중 충돌도 있었는데 인벤토리와 상점의 아이템을 통합 시키고 자신이 보유하고 있는지로 판단할 것인지와 인벤토리의 아이템에서 중복이면은 아이템 갯수를 더 늘리지 않고 Count 변수만 늘릴 것인지 하는 기획적인 의견 대립이었다. 팀원분들과 잘 이야기 해서 작업하기 편하게 인벤토리와 상점은 통합시키고 중복 아이템 개수는 소비 아이템만 Count를 늘리는 방식을 택하게 되었다.

 내일은 추가 구현이나 완성도를 높일 생각이다. 일단 보이는 지점들은 아이템에 있어서 너무 뒤죽박죽으로 정리가 안 되어 있는데 정렬이 좀 필요한 것 같고 겉으로 보이는 줄 바꿈이나 안내창 UI의 완성도를 높일 생각이다. 내일도 파이팅!

 

 

참고 : 

https://stackoverflow.com/questions/5240143/invalidcastexception-unable-to-cast-objects-of-type-base-to-type-subclass

 

InvalidCastException: Unable To Cast Objects of type [base] to type [subclass]

I have a custom CustomMembershipUser that inherits from MembershipUser. public class ConfigMembershipUser : MembershipUser { // custom stuff } I am using Linq-to-SQL to read from a database a...

stackoverflow.com

 

+ Recent posts