오늘의 학습 키워드

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;
}

잘 되는 모습

 

 

오늘의 회고

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

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

한 주 회고

이번 주에는 TextRPG를 콘솔로 구현해보는 시간을 가졌다.

이전에 비슷한 것을 해보았지만 더 깊이 들어가서 고민을 많이 한 시간이어서 더 좋았다. (특히 구조)

 

피드백(개인 과제)

 개인 과제 수행을 바탕으로 피드백을 받을 수 있었는데 정말 피가 되고 살이 되는 조언들이었다. 대표적으로 내가 놓친 부분들(불필요한 주석, 불필요한 using문, 인터페이스 변수명 명확화) 등 관성으로만 또는 귀찮아서 안 한 것들이 코드 가독성을 해치게 된다는 사실을 알고 바로 적용하기로 했다. 나머지로 정보은닉이나 콜백 남용이 조금 있었다는 피드백이 있었는데 모두 팀 프로젝트에 체크하면서 적용하고 계속 습관화 시키도록 해야겠다.

 

다음 주 목표

다음 주는 팀 과제로 TextRPG를 팀원과 같이 구현하게 된다. 나 뿐만 아니라 다른 사람들과의 협업에서 더 많은 기능과 더 많은 아이디어를 구현할 수 있게 되어 좋다. 탄력은 받았으니 계속 달리기만 하면 될 것 같다. 다음 주도 파이팅!

오늘의 학습 키워드

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를 만들게 되는데 기대가 되고 개인으로 만들던 경험을 기반으로 더 잘 만들 수 있을 것 같다. 다음 주도 파이팅!

오늘의 학습 키워드

struct, StackOverFlow

공부한 내용

리팩토링 구상

리팩토링을 하기 전에 구조 부터 문제라는 생각이 들어 간단한 시스템 흐름도를 만들어 보았다.

아래 그림으로 보면 별 문제가 없어보이지만..

문제 1 : 너무 많은 gameState

모든 상태를 gameState에 넣어놓고 현재 gameState에 따라 함수로 진행하는 방법이다.

이러면 분기가 하나 늘어날 때마다 gameState가 많아질 것이며 코드의 복잡도가 더 증가할 것이라고 판단했다.

이것에 대한 내가 생각한 아이디어 중 하나는 다음 단계로 이동할 때 현재의 씬을 스택에 담는 것이다.

스택에 담는다면 나가기 버튼을 눌렀을 때도 이전 씬을 담으니까 그 씬을 돌려주면 된다.

// 현재 게임 상태
    GameState gameState;

    // UI랑 게임 플레이랑 나눌 필요성이 있어보임
    // 문제점 : 게임 스테이트가 너무 많음
    // 아이디어 1: 연결 되어있는 것이라면 Stack으로 씬을 담는 것

    public enum GameState
    {
        Town,
        State,
        Inventory,
        Equipment,
        ItemSort,
        Shop,
        Purchase,
        Sell,
        Dungeon,
        INN,
    }

    public void Start()
    {
        JsonUtility.Save(character.Info);


        switch (gameState)
        {
            case GameState.Town:
                EnterTown();
                break;
            case GameState.State:
                ShowState();
                break;
            case GameState.Inventory:
                ShowInventory();
                break;
            case GameState.Equipment:
                ManageEquiment();
                break;
            case GameState.ItemSort:
                ItemSort();
                break;
            case GameState.Shop:
                ShowShop();
                break;
            case GameState.Purchase:
                PurchaseItem();
                break;
            case GameState.Sell:
                SellItem();
                break;
            case GameState.Dungeon:
                EnterDungeon();
                break;
            case GameState.INN:
                EnterINN();
                break;
            default:
                break;
        }
    }

gameState를 메인에서 선택하는 5가지로 줄이니 우측 스크롤 바 쪽에 빨간줄을 여럿 볼 수 있다. ㅠㅠ

나의 과오니 내가 치워야지..

* 추가 : 어차피 맨 처음에 마을으로 가니까 gameState가 꼭 필요하지 않다고 판단해서 gameState 관련 로직을 제거하게 됐다.

gameState 관련 로직을 빼도 나가기가 잘 되는 모습

 

문제 1-1 : 호출 스택 문제 (스택 오버 플로우)

만들면서 뭔가 쎄한 느낌이 들어서 확인해보니 마을로 가지 않고서는 호출 스택을 탈출할 수 없다는 문제가 있었다.

'함수 호출으로 넘어가는 것이 문제인가?' 라는 생각이 들어 바로 적용해보기로 했다.

그럼 그렇지 내가 너무 복잡하게 생각해서 돌아가는 경우였다. 그냥 return 하면 되는 쉬운 방법이 있었다니!

함수에서 다른 함수로 이동할 때 다시 이전 함수로 돌아가는 경우라면 그냥 return을 쓰면 된다. 어렵게 생각하지 말자!

 

문제 2 : gameState와 맞물려 있는 안내 UI

일단 만들고 나중에 고치자고 생각했지만 만들면서도 계속 반복되는 게 많아 불편한 마음이 들었던 UI도 문제였다.

아래의 2개의 코드는 각각 마을과 아이템 장착 관리인데 비슷한 부분이 많아 변수를 넘겨주는 방식으로 통합하려 한다.

Console.Write
(
    "스파르타 마을에 오신 여러분 환영합니다\n" +
    "이곳에서 던전으로 들어가기 전 활동을 할 수 있습니다.\n\n" +
    "1. 상태보기\n" +
    "2. 인벤토리\n" +
    "3. 상점\n" +
    "4. 던전입장\n" +
    "5. 휴식하기\n" +
    "\n" +
    "원하시는 행동을 입력해주세요.\n" +
    ">> "
);
Console.Write
(
    titleStr +
    "보유 중인 아이템을 관리할 수 있습니다.\n\n" +
    "[아이템 목록]\n" +
    itemListStr +
    choiceStr +
    "원하시는 행동을 입력해주세요.\n" +
    ">> "
);

형식이 비슷하니 하나에서 다 출력하도록 바꿨다.

void WriteUI(string title, string introduction, string result, string choice, string inputStr)
{
    Console.WriteLine(title + introduction + result + choice + inputStr);
}

 

문제 3 : 너무 많은 아이템 클래스

이전엔 Item 클래스를 상속 받는 객체를 따로 생성하여 받는 방식으로 구현 하였는데 객체가 생길 때마다 생성 해야 되는 문제가 있었다.

이미 Item 클래스에 SetInfo라는 함수가 있으니 생성자로 접근하여 세팅하도록 바꾸려고 한다.

불필요한 객체 생성을 줄이고 생성자로 정보를 넘겼다.

public ItemShop()
{
    ItemList.AddRange(new Item[] 
    { 
        new Item(ItemType.Armor, "수련자 갑옷", 0, 5, "수련에 도움을 주는 갑옷입니다.", 1000), 
        new Item(ItemType.Armor, "무쇠갑옷", 0, 5, "무쇠로 만들어져 튼튼한 갑옷입니다." , 600), 
        new Item(ItemType.Armor, "스파르타의 갑옷", 0, 15, "스파르타의 전사들이 사용했다는 전설의 갑옷입니다.", 3500), 
        new Item(ItemType.Weapon, "낡은 검", 2, 0, "쉽게 볼 수 있는 낡은 검입니다.", 600), 
        new Item(ItemType.Weapon, "청동 도끼", 5, 0, "어디선가 사용됐던 것 같은 도끼입니다.", 1500), 
        new Item(ItemType.Weapon, "스파르타의 창", 7, 0, "스파르타의 전사들이 사용했다는 전설의 창입니다.", 2400) 
    });
}

 

문제 4 : 변수 접근 제한

이전에 그냥 Get Set 프로퍼티로 접근했는데

데이터 저장을 위해 프로퍼티로 넘긴다고 하면 Set은 Private로 해주고 실수 방지를 위해 변수가 아닌 함수로만 통해 접근하는 것이 좋아 보였다.

구조체도 인터페이스 상속이 가능하다는 것을 이번에 튜터님께 질문 하면서 알게 되었다.

추가로 함수도 사용 가능하다.

public struct 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;
    }
}

 

 

오늘의 회고

 오늘은 리팩토링 하는 시간을 가졌다. 리팩토링을 끝내고 리드미 파일도 정리하고 시간이 남으면 알고리즘도 공부해보려 했는데 시간이 부족해 리팩토링도 다 하지 못했다. 아쉽지만 오히려 혼자 고민 해본 것과 튜터님께 질문 드린 것을 통해  배운 것이 많아서 아쉬울 건 없는 하루였던 것 같다.

 내일은 리팩토링(데이터 저장, 상태 콜백, 클래스 나누기)를 하고 리드미 파일을 작성하려고 한다. 내일도 파이팅!

 

 

 

오늘의 학습 키워드

JsonSerializer.Serialize

공부한 내용

목표 : 세이브와 로드

시도한 것 : 저장할 때 JsonSerializer.Serialize(클래스)로 불러오려고 했다.

문제 : JsonSerializer.Serialize는 클래스로 불러오면 클래스 안의 구조체까지는 접근하지 못한다. 또한 프로퍼티 값이어야 읽어올 수 있다.

해결 : 사용할 때 JsonSerializer.Serialize(클래스명.클래스구조체)로 세이브 했다.

public struct CharacterInfo
{
    public int Level { get; set; }
    public string Name { get; set; }
    public float Attack { get; set; }
    public float Defense { get; set; }
    public float Health { get; set; }
    public float Gold { get; set; }
    public bool IsDead { get; set; }
    public float MaxHealth { get; set; }
    public float DefaultAttack { get; set; }
    public float DefaultDefense { get; set; }
}
class JsonUtility
    {
        public static void Save<T>(T t)
        {
            string json = JsonSerializer.Serialize(t);
            File.WriteAllText(@"D:\data.json", json);
        }

        public static T Load<T>(string path)
        {
            var json = File.ReadAllText(path);
            if (json != null)
            {
                return JsonSerializer.Deserialize<T>(json);
            }
            else
            {
                return default(T);
            }
        }
    }
static void Main(string[] args)
{
    Console.ForegroundColor = ConsoleColor.Green;

    Game game = new Game();

    // TODO : 추후 캐릭터 선택창 만들 것
    // 시작 : 캐릭터를 선택하시오
    // 끝 : 캐릭터 인스턴스 생성
    character = new Warrior();

    var info = JsonUtility.Load<Character.CharacterInfo>(@"D:\data.json");
    if (info.Name != null)
    {
        character.Info = info;
    }

    while (true)
    {
        game.Start();
    }
}

개인 과제 질문 피드백

기능 구상이 어려울 때

- 처음 기능이 끝까지 갈 수가 없음
    - 나중에 바뀌기 마련
    - 개방 폐쇄 원칙(Open Close) : 확장에는 개방 수정에는 폐쇄
- 플로우 차트 만들어보기 (와이어 프레임)

 

변수 정하기

- 왼쪽과 오른쪽은 기획에서 달라질 수 있는 부분이다.

- 왼쪽이 필요한 경우라면 공격력이 필요한 방어구(가시방패)가 필요 한다던지 하는 경우

- 오른쪽은 그런 것이 필요하지 않을 때

가장 중요한 것은 왜 이 방식을 선택했는지에 대한 근거가 필요하다.

클래스를 어떻게 나누는가?

- 외부로 둔다면 영향을 줄 수 있는 범위가 커진다.
- 최대한 분리한다면 영향을 적게 준다. 하지만 접근하는데 불편할 수 있다.

- 이것도 상황 판단 하에 정하기 -> 장단점을 파악하면서 사용

 

추가 : if else와 try catch문 어떨 때 쓰는 게 좋은가

- 입력을 받았을 때 입력이 예상 가능하면 if문
- 입력이 예상 불가능 하면 try catch문

오늘의 회고

 오늘은 TextRPG 구현을 마무리하고 제출하는 날이었다. 시간이 촉박했기에 부랴부랴 만드느라 코드에 대한 고민이 조금 부족했던 것 같다. 다른 사람의 코드도 보면서 좋은 방법들이 있다면 질문해보고 내 것에 적용하는 시간(리팩토링)을 가져야겠다. 팀 프로젝트도 TextRPG의 연장선으로 진행한다고 하는데 좋은 방법들을 많이 적용하도록 하고 글로 또 남겨서 헷갈릴 때마다 다시 보고 해야겠다.

 내일은 이전에 언급했다싶이 리팩토링을 진행할 생각이다. 또한 깃허브에 ReadMe파일에 정리하는 시간을 가지도록 해야겠다. 내일도 파이팅!

 

 

깃허브 링크 :

https://github.com/JeongJuChan/TextRPG_Single

 

GitHub - JeongJuChan/TextRPG_Single

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

github.com

 

 

참고 :

https://www.csharpstudy.com/Data/Json-SystemTextJson.aspx

 

System.Text.Json - C# 프로그래밍 배우기 (Learn C# Programming)

System.Text.Json .NET Core 3.0에서 새로운 Json 지원 클래스로서 System.Text.Json가 내장(built-in) 되었으며, ASP.NET Core 3.0 에서 디폴트 Json Serializer가 서드파티의 Newtonsoft.Json에서 System.Text.Json 으로 변경되었다

www.csharpstudy.com

 

 

오늘의 학습 키워드

Int int.ComareTo(int value), List<T>.Sort(Comparison<T> comparison), Console.ForegroundColor

공부한 내용

목표 : 인벤토리를 내림차순으로 정렬

시도한 것 : List.Sort()와 람다를 사용하여 Int int.CompareTo(int value)로 앞의 int와 뒤의 int를 비교하여 정렬하였다.

문제 : 기본적으로 비교하면 오름차순으로 정렬된다.

해결 : -를 곱하고 반환해 반대로 정렬하게 하였다.(내림차순)

// 아이템 정렬
void ItemSort()
{
    string input = null;
    WriteInventoryInfo();

    input = Console.ReadLine();

    switch (input)
    {
        case "0":
            gameState = GameState.Inventory;
            break;
        case "1":
            inventory.Items.Sort((x, y) =>
            {
                // ComareTo
                // -는 내림차순
                return -x.Info.Name.Length.CompareTo(y.Info.Name.Length);
            });
            break;
        case "2":
            inventory.Items.Sort((x, y) =>
            {
                // ComareTo
                return -x.Info.IsEquiped.CompareTo(y.Info.IsEquiped);
            });
            break;
        case "3":
            inventory.Items.Sort((x, y) =>
            {
                // ComareTo
                return -x.Info.AttackModifier.CompareTo(y.Info.AttackModifier);
            });
            break;
        case "4":
            inventory.Items.Sort((x, y) =>
            {
                // ComareTo
                return -x.Info.DefenseModifier.CompareTo(y.Info.DefenseModifier);
            });
            break;
        default:
            Console.WriteLine("잘못된 입력입니다.\n");
            break;
    }
}

목표 : 콘솔 색 바꾸기

해결 : Console.ForegroundColor = ConsoleColor.Green;을 사용하여 콘솔 색을 초록색으로 바꾸었다.

 

오늘의 회고

 오늘은 상점 구매와 판매, 정렬을 구현하고 줄맞춤 버그를 수정했다. 정렬에서 Comparison을 사용하는데 좀 걸렸지만 int 값을 반환하는 것을 알고 내림차순으로 쉽게 정렬할 수 있었다. 줄 맞춤 버그는 어제부터 시달렸던 과제인데 한글이 2바이트를 먹는다는 것과 한글을 검출해서 공백에 패딩을 한 자리 주는 것으로 해결할 수 있었다.

 내일은 장착 개선, 던전입장, 휴식, 레벨업, 게임 저장을 해볼 생각이다. 이를 내일 제출(오후 6시)까지 구현하는 것은 어려울 수 있지만 최대한 구현해 볼 생각이고 이후로도 이번주 안에는 작업할 생각이다. 내일 열심히 해서 최대한 달성해보자. 내일도 파이팅!

 

 

참고 :

https://developer-talk.tistory.com/220

 

[C#]List 속성별로 정렬

이번 포스팅에서는 C#에서 속성별로 List 객체를 정렬하는 방법을 설명합니다. LINQ OrderBy() 메서드를 사용하여 정렬된 새로운 List를 생성하는 방법이 있으며, 기존 List를 정렬하는 List에 내장된 Sort

developer-talk.tistory.com

 

오늘의 학습 키워드

string.PadRight(int num, char character), Encoding.Unicode.GetByteCount(string str)

 

공부한 내용

문제 : 오늘은 인벤토리 구현 중에 문제를 맞닥뜨렸다. 그것은 아이템 목록을 정렬하는 것인데 여러 아이템을 정렬된 상태로 출력 하는 것이었다.

시도한 것 1 : 처음엔 그냥 저번에 배웠듯이 PadRight를 사용하기로 했다. 하지만 아래에 나와있듯이 자꾸 무쇠 갑옷이 삐져나오는 문제가 생겼다.

찾아본 원인 : C#에서는 한글과 다른 문자들의 바이트 크기가 달라서 생기는 문제라는 것이다. (한글 2바이트, 다른 것 1바이트)

또 다른 문제 : 하지만 생각했던 대로 바이트 개수가 나오지 않았다. (한글 3바이트, 다른 것 1바이트)

원인 : 이는 Encoding.Default가 UTF-8로 잡혀있어서 한글은 3바이트로 나오던 것이다.

시도한 것 2 : 그래서 Encoding.Unicode로 지정했더니 모든 문자가 2바이트로 나오게 되었다.

마지막 문제 : 한글이 2문자를 차지하니 한글과 공백을 구분하는 로직이 필요하다.

시도한 것 3 : 한글을 EncodingDefault.GetByteCount == 3인지 검사해 개수를 세고 -1한 값을 패딩을 줄 전체 바이트 길이에서 빼줬다. (한글이 2글자 공백이 1글자를 차지하므로 공백일 시 패딩을 1개씩 늘림)

string ArrangeInfo(string str, int totalLength, char padChar = ' ')
{
    // 한글 2바이트
    int krByteLength = totalLength;
    int byteCount = Encoding.Unicode.GetByteCount(str);
    int krCount = 0;

    string[] strArr = str.Split();

    foreach (var c in str)
    {
        // 한글이면 count++;
        if (Encoding.Default.GetByteCount(c.ToString()) == 3)
        {
            krCount++;
        }
    }

    // 정렬이 안 되는 문제 발생
    // 2바이트니 / 2 할 때 한글의 개수가 홀수면 1 짝수면 2씩 감소시키기
    // 12
    //byteCount = krCount * 2;

    // 공백, 영어, 숫자 1바이트
    // 한글은 2바이트 크기는 2배
    // 한글 10글자 기준 30바이트
    // 한글 4개 12바이트
    // 8 8 10 10 14
    //int bytes = Encoding.Unicode.GetByteCount("무쇠갑옷");
    //int bytes1 = Encoding.Unicode.GetByteCount("낡은 검");
    //int bytes2 = Encoding.Unicode.GetByteCount("린넨 셔츠");
    //int bytes3 = Encoding.Unicode.GetByteCount("나무 망치");
    //int bytes4 = Encoding.Unicode.GetByteCount("스파르타의 창");

    // * 조심 : Default시 UTF-8로 잡혀서 3바이트로 나옴 
    // int bytes = Encoding.Default.GetByteCount("무쇠갑옷");
    string tempStr = str.PadRight(totalLength + totalLength - krCount, padChar);

    return tempStr;
}

 

 

오늘의 회고

 오늘은 인벤토리와 장착 기능 구현을 마무리 했다. 추가적으로 구현할 기능들이 여럿 보이지만 일단 선택 구현 사항부터 마무리 지으려고 한다. 오늘은 문자열 정렬 쪽에서 시간을 많이 뺏긴 것 같은데 그만큼 내가 모르고 있었다는 것이니까 다음 번에는 틀리지 않도록 다시 공부해봐야겠다.

 내일은 콘솔 쪽을 꾸미고 인벤토리를 정렬하는 부분을 좀 해봐야겠다. 정렬 쪽은 특히나 게임 쪽에서 자주 나오는 개념이니 만큼 모른다면 확실히 알아가고 구현이 가능하다면 어떻게 하면 더 나은 구현 방법일지를 고민해봐야겠다. 내일도 파이팅!

 

 

참고 : 

https://holjjack.tistory.com/145

 

[C#] 한글 2byte 로 계산 하는 방법

한글을 2byte로 계산하기위해서 기본적으로 이해를 하셔야 하는 부분은 바로 Unicode 입니다. 해당 부분에 대한 이해없이 단순히 Encoding을 이용해서 Byte 수를 계산한다면, 한글자당 2~3 으로 byte수가

holjjack.tistory.com

 

한 주 회고

이번 주에는 알고리즘에 대해 배우고 C#으로 게임을 구현해보는 시간을 가졌다.

알고리즘 사이트로 LeetCode가 있다는 것을 알게 되고 거기서 알고리즘 문제도 풀어봤다.

비슷한 유형의 문제들을 풀면서 패턴을 찾아내고 적용하는 것이 제일 빨리 실력이 느는 것 같다.

또한 C#으로 게임을 구현해보기도 했는데

유니티로만 구현하는 것도 좋았지만 C# 자체로 구현하는 것도 나에게 도전이 되고 공부가 된 것 같다.

 

 

다음 주 목표

TextRPG를 구현 하는 것이 이번 주의 개인 과제인데 구현하는 것은 고민이 많이 필요하지만 재밌는 것 같다. 다음 주에 명세서까지 구현이 끝나면 샤이닝 포스 같은 게임처럼 컨셉을 추가하던가 인터페이스 등으로 코드 리팩토링을 하는 작업을 해야겠다.

오늘의 학습 키워드

ToString("D2"), string.PadLeft(int num, char character), 인터페이스

 

공부한 내용

문자열 공백 0출력

저번에 배웠던 ToString("N2")는 소숫점을 나타낸다면 ToString("D2")를 하면 공백이 0으로 채워진다는 것을 배웠다.

void ShowState()
{
    string input = null;
    while (input != "0")
    {
        Console.Write
        (
            "상태 보기\n" +
            "캐릭터의 정보가 표시됩니다\n\n" +
            $"Lv. {character.Level.ToString("D2")}\n" +
            $"Chad ( {character.Name} )\n" +
            $"공격력 : {character.Attack}\n" +
            $"방어력 : {character.Defense}\n" +
            $"체력 : {character.Health}\n" +
            $"Gold : {character.Gold} G\n\n" +
            "0. 나가기\n\n" +
            "원하시는 행동을 입력해주세요.\n" +
            ">> "
        );

        input = Console.ReadLine();
    }

    Console.WriteLine();
}

아이템 인터페이스

인터페이스에 관해 궁금한 점이 있었는데 어떨 때 인터페이스를 사용하는지 궁금했다.

검색도 해보고 고민을 해봤지만 이건 내 영역을 넘어선 것 같아서 튜터님께 언제 사용하면 좋을지에 대해 질문을 드렸다.

인터페이스 사용과 관련하여 다음 내용을 참고하면 좋다.

1. 인터페이스는 공통적인 행위를 묶을 때 사용하면 좋다.

2. 로직이 조금이라도 들어간다면 클래스로 빼는 것이 덜 복잡할 수 있다.(인터페이스 안에서는 구현이 불가능하므로)

3. 최소한의 단위로 만들어야지 코드가 보기 좋다.

interface IItem
{
    void Equip(bool isEquip);
}

보기 안 좋은 코드일 수 있지만 일단 구현해보는 것으로 하고 나중에 리팩토링을 해봐야겠다고 생각했다.

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

namespace TextRPG_Single
{
    class Item : IItem
    {
        public enum ItemType
        {
            Weapon,
            Armor,
        }

        public struct ItemInfo
        {
            public ItemType Type { get; set; }
            public string Name { get; set; }
            public int AttackModifier { get; set; }
            public int DefenseModifier { get; set; }
            public bool IsEquiped { get; set; }
        }

        public ItemInfo Info = new ItemInfo();
        

        protected Item(ItemType type, string name, int attack, int defense, bool isEquiped)
        {
            Info.Type = type;
            Info.Name = name;
            Info.AttackModifier = attack;
            Info.DefenseModifier = defense;
            Info.IsEquiped = isEquiped;
        }

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

 

오늘의 회고

 튜터님께 질문드리기 전에 내가 고민했던 것들을 정리 했었는데 막상 앞에서 질문하려니 부끄럽고 말도 잘 못드렸던 것 같다. 하지만 친절하게 잘 설명해주려고 하시고 동기부여도 해주시니 공부할 맛이 났다. 사실 엄청나게 깨달음을 주신 건 아니지만 코드엔 정답은 없고 상황마다 다르며 직접 구현해보고 내가 경험으로 쌓아가는 것이라 먼저 해보라는 말씀이 제일 기억에 남았던 것 같다. 무작정 짜보고 왜 이게 안 좋은지 판단하고 고치는 작업을 통해 내가 더 성장할 수 있는 기회를 주신 것 같아서 더 감사하다는 생각이 들었다.

 오늘 질문을 하면서 느낀 점은 튜터님이 있고 질문을 편하게 드릴 수 있다는 점이 얼마나 좋은 환경인지 조금이라도 깨닫게 되는 계기가 된 것 같다. 항상 열심히 해주시는 튜터님들, 매니저님들, 동료들을 생각하며 나도 또한 열심히 해야겠다. 내일도 파이팅!

 

 

참고 : 

https://acpi.tistory.com/122

 

[C#] 숫자앞에 0붙이기

1. PadLeft 함수 사용 int value = 100; string result = Convert.ToString(value).PadLeft(5, '0'); MessageBox.Show(result); 2. string.Format 함수 사용 int value = 100; string result = string.Format("{0:D5}", value); MessageBox.Show(result); 3. ToString

acpi.tistory.com

 

+ Recent posts