오늘의 학습 키워드

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

 

 

오늘의 회고

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

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

오늘의 학습 키워드

참조

 

공부한 내용

중복 장착 버그

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

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

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

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

 

오늘의 학습 키워드

JsonSerializer.Deserialize

 

공부한 내용

JsonSerializer.Deserialize InvalidOperationException 오류

json 파일을 Deserialize 해서 읽어오는 과정 중 오류를 맞닥뜨렸다. 오류 내용은 생성자의 파라미터가 오브젝트 프로퍼티나 필드에 바인드 되어야 한다고 써있다.

아래 처럼 생성자의 매개변수 hp가 프로퍼티 이름과 맞지 않는다면 오류를 반환하게 된다는 것을 알게 되었고 이름을 맞춰주었다.

public Character(string name, string job, int level, int atk, int def, int hp, int maxMp, int gold)
{
    Name = name;
    Job = job;
    Level = level;
    Atk = atk;
    Def = def;
    MaxHp = hp;
    CurrentHp = hp;
    MaxMp = maxMp;
    CurrentMp = maxMp;
    Gold = gold;
}
public Character(string name, string job, int level, int atk, int def, int maxHp, int maxMp, int gold)
{
    Name = name;
    Job = job;
    Level = level;
    Atk = atk;
    Def = def;
    MaxHp = maxHp;
    CurrentHp = maxHp;
    MaxMp = maxMp;
    CurrentMp = maxMp;
    Gold = gold;
}

잘 되는 모습

 

 

오늘의 회고

 오늘은 깃 머지와 포션 힐링, 버그 수정을 했다. 추가 구현을 하기 위해 팀원들이 만든 것들과 내 것을 합치는데 컨플릭이 굉장히 많이 떠서 곤란했다. 천천히 코드를 읽어나가면서 중복되는 부분을 걸러내고 바뀐 부분들을 통합했더니 별 문제 없이 통합할 수 있었다.

 내일은 만든 스킬 구현을 배틀 쪽에 통합하는 것을 하려 한다. 아마 겹치는 기능을 수정 하게 될텐데 오늘 처럼 팀원에게 물어보면서 통합하는 방식으로 진행해야겠다. 내일도 파이팅!

오늘의 학습 키워드

Callback

 

공부한 내용

단일 타겟 스킬과 다중 타겟 스킬 나누기

단일 타겟 스킬과 다중 타겟 스킬을 나누기 위해 공통이라는 Skill을 두고 타입에 따라 스킬을 사용하는 콜백을 등록해 사용하도록 구현하였다.

나중에 배틀 로직을 구현하는 팀원의 작업이 끝났을 때 콜백으로 받을지 말지를 정해야겠다. - 이번 개인 과제 피드백 시간에 콜백이 디버깅 과정에서 추적하기 어렵다는 단점이 있다고 배웠다.

namespace TextRPG_Team
{
    public enum SkillType
    {
        SigleTarget,
        MultipleTarget
    }

    public class SigleSkill : Skill
    {
        Action<string, float, float> skill;

        public SigleSkill(string name, string description, int cost, float damage, float damageMod, Action<string, float, float> skillAction) : base(name, description, SkillType.SigleTarget, cost, damage, damageMod)
        {
            skill = skillAction;
        }

        public void UseSkill(string target)
        {
            skill?.Invoke(target, Damage, DamageMod);
        }
    }

    public class MultipleSkill : Skill
    {
        Action<List<string>, float, float, int> mutipleSkill;

        public MultipleSkill(string name, string description, int cost, float damage, float damageMod, Action<List<string>, float, float, int> mutipleSkill) : base(name, description, SkillType.SigleTarget, cost, damage, damageMod)
        {
            this.mutipleSkill = mutipleSkill;
        }

        public void UseSkill(List<string> targets, int targetCount = 2)
        {
            mutipleSkill?.Invoke(targets, Damage, DamageMod, targetCount);
        }
    }

    public class Skill
    {
        // 정보 MP 추가
        // 캐릭터에서 가져오기
        // 2. 스킬 항목 추가
        
        // TODO :
        // Program에 있는 배틀 로직 배틀 쪽에 붙이기

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

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

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

    }
}

 

타겟은 CharacterSkills 객체에서 단일 타겟 인지 다중 타겟 인지에 따라 데미지를 입게끔 구현 하였다. (타겟은 객체인데 임시로 string이라고 해놨음)

namespace TextRPG_Team
{
    public class CharacterSkills
    {
        public void AttackSigleTarget(string target, float damage, float damageMod)
        {
            damage *= damageMod;
            //target.TakeDamage(damage);
        }

        public void AttackMutipleTarget(List<string> targets, float damage, float damageMod, int targetCount = 2)
        {
            List<string> newTarget = new List<string>(targets);
            damage *= damageMod;
            int count = 0;
            while (count < newTarget.Count)
            {
                int index = Utility.rand.Next(0, count);
                //targets[index].TakeDamage(damage);
                newTarget.Remove(newTarget[index]);
                count++;
            }
        }
    }
}

 

크리티컬 공격과 회피

이 또한 배틀 구현과 합쳐졌을 때 적용할 코드들이며 어느 클래스에 합쳐질지는 머지 이후에 확인할 것이다. (일단 더미 배틀 로직이라고 해놨음)

#region 배틀구현 이후 추가할 메서드들 - 주찬

int criticalPercentage = 15;
int dodgePercentage = 10;
float criticalMod = 1.6f;

void DummyBattleLogic()
{
    // 크리티컬 데미지
    float damage = 0;
    damage = IsCritical() ? GetCriticalDamage(damage) : damage;

    // 피하기 로직
    string attackSentence = "~을";
    attackSentence += IsDodged() ? GetDodgeSentence() : "";
}

bool IsCritical()
{
    int randomPercentage = Utility.rand.Next(0, 100);
    if (randomPercentage < criticalPercentage)
    {
        return true;
    }
    else
    {
        return false;
    }
}

float GetCriticalDamage(float damage)
{
    return damage * criticalMod;
}

bool IsDodged()
{
    int randomPercentage = Utility.rand.Next(0, 100);
    if (randomPercentage < dodgePercentage)
    {
        return true;
    }
    else 
    { 
        return false; 
    }
}

public string GetDodgeSentence()
{
    return "공격했지만 아무일도 일어나지 않았습니다.";
}
#endregion

 

 

오늘의 회고

 오늘은 새로운 팀원과 함께 하는 TextRPG 구현의 기반을 다졌다. 저번에 해봤던 Git으로 하는 미니 팀 프로젝트와 개인 과제로 주어진 TextRPG를 수행하고 나니까 작업이 훨씬 더 빠르게 진행 되어서 좋았다. 또한 이미 구현한 것을 더 좋게 만드는 과정도 더 깊은 고민을 할 수 있게 해서 더 좋은 시간이었다.

 내일은 아마 다른 팀원분들이 구현한 것과 합치고 수정하는 작업 + 소비 아이템 관련해서 작업이 예상되는데 재미있을 것 같다. 내일도 열심히 해보자!

오늘의 학습 키워드

Action,

공부한 내용

아이템 -> 인벤토리 (장착 로직과 캐릭터 상태 갱신 콜백)

인벤토리의 리스트로 있는 아이템에 접근해서 콜백을 주기에는 아이템마다 세팅 해줘야하고 나중에 있을지도 모르는 캐릭터마다 상태를 갱신시켜주는 상황이 생길 수 있다.

이를 해결하기 위해 인벤토리에서 장착 콜백을 받아놓고 처리하는 방식으로 진행하려고 한다.

public class ItemInfo : IItem
{
    public ItemType Type { get; private set; }
    public string Name { get; private set; }
    public int AttackModifier { get; private set; }
    public int DefenseModifier { get; private set; }
    public string Description { get; private set; }
    public int Price { get; private set; }
    public bool IsEquiped { get; private set; }

    public void SetInfo(ItemType type, string name, int attack, int defense, string description, int price, bool isEquiped)
    {
        Type = type;
        Name = name;
        AttackModifier = attack;
        DefenseModifier = defense;
        Description = description;
        Price = price;
        IsEquiped = isEquiped;
    }

    public void Equip(bool isEquip)
    {
        IsEquiped = isEquip;
    }
}

인벤토리에 옮긴 후

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;

namespace TextRPG_Single
{
    class Inventory
    {
        Action<Item> equipCallback;

        public List<Item> Items { get; private set; } = new List<Item>();

        public Inventory()
        {
            Items.Add(new Item(ItemType.Armor, "린넨 셔츠", 0, 1, "부드러운 천으로 만든 셔츠이다.", 100));
            Items.Add(new Item(ItemType.Weapon, "나무 망치", 1, 0, "나무로 만든 망치이다.", 200));
        }

        public void Init(Character character)
        {
            equipCallback = character.ApplyToState;
        }

        public void Equip(Item item)
        {
            item.Info.IsEquiped = true;
            equipCallback?.Invoke(item);
        }

        public void UnEquip(Item item)
        {
            item.Info.IsEquiped = false;
            equipCallback?.Invoke(item);
        }

        public int GetSellingPrice(Item item)
        {
            return (int)(item.Info.Price * 0.85f);
        }

        public void RemoveItem(Item item)
        {
            UnEquip(item);
            Items.Remove(item);
        }

        public Item GetSameTypeItem(Item otherItem)
        {
            foreach (var item in Items)
            {
                if (item != otherItem && item.Info.Type == otherItem.Info.Type && item.Info.IsEquiped)
                {
                    Equip(item);
                    return item;
                }
            }

            return null;
        }
    }
}

 

 

오늘의 회고

 오늘은 리팩토링 하는 마지막 주차이다. 다른 것들은 모두 이해가 갔지만 Json Deserialize 하는 부분에서 프로퍼티 쪽으로 데이터를 넘기는 부분은 좀 더 공부가 필요한 것 같다. 그래도 목표는 거의 다 달성했으니 꽤나 만족하는 하루였다.

다음 주는 다른 사람들과 팀으로 TextRPG를 만들게 되는데 기대가 되고 개인으로 만들던 경험을 기반으로 더 잘 만들 수 있을 것 같다. 다음 주도 파이팅!

+ Recent posts