1. 설계 
2. 트리구조와 무슨 연관? 
3. 퀘스트 수락 -> 퀘스트 완료 -> 연계 퀘스트 추가 플로우
4. 구현 코드
5. 마무리

 

📝 1. 퀘스트 시스템

🔖 설계

 

QuestState 

          ■ 퀘스트의 상태를 나타내기 위해 만든 enum  

          ■ 받기전, 받은 후, 보상 수령 가능한 상태, 수령받은 후 끝난 상태

QuestClass 클래스

           abstract 클래스

           '퀘스트 완료 조건 메서드'를 저장하는 Func<bool> completeQuest 를 필드로 가지고 있음 

           TodoMission() 메서드

                       하위클래스에서 '퀘스트 완료 조건' 구현 

                        조건이 만족하면 => true 반환

                        조건이 불만족하면 => false 반환

 MonsterKillQuest 클래스 / EquiptQuest 클래스

           TodoMission () 메서드를 오버라이딩 

QuestManager 클래스

           List<Quest> performedQuest 

                        수행가능한 퀘스트 클래스들을 담을 컨테이너 

 

📝 2. 트리랑 무슨 연관?

: 연계퀘스트를 어떻게 구성할지 다이어그램을 작성하다가 이 구조가 트리와 굉장히 닮았다고 생각했다.

: 퀘스트에서 트리구조를 사용하면 몇 가지 장점이 있다.

1. 부모퀘스트는 자식에 해당하는 퀘스트들을 관리할 수 있다.

2. 자식에 해당하는 퀘스트는 부모퀘스트 정보를 알 수 있다.

3. 자식에 해당하는 퀘스트는 부모퀘스트를 트리에서 탐색(search) 할 수 있다.

 

🔖 그냥 리스트 사용하면 안 되나?

그럼 해당 퀘스트가 어떤 퀘스트로부터 파생되었는지 알기 힘들다.

 

예를 들어서, 수행하지 못하는 퀘스트도 화면에 표시한다고 생각하자

------------------

[퀘스트]

1. Armor 장비 한 개 착용                 [완료가능]

2. Weapon 장비 한개 착용             [수락가능]

3. 인벤토리에 15개 이상 장비 착용  [수행불가]

------------------

이미지상 '인벤토리에 15개 이상 장비 착용'퀘스트는 '무기 장비 1개 착용'퀘스트 후에 수행가능한 연계퀘스트이다.

즉 , '무기 장비 1개 착용'을 완료해야 퀘스트 수락이 가능하다.

이때, ' 인벤토리에 15개 이상 장비 착용' 퀘스트가 어느 퀘스트로부터 파생되었는지 알 수 있는 방법이 없다.

그래서 트리 구조로 [부모퀘스트] [자식퀘스트]를 연결시키는 것이다.

 

📝 3. 수락 완료  연계퀘스트 추가 플로우

: 주의 깊게 봐야 할 부분은 빨간색 박스 부분이다.

: 퀘스트를 완료했다면 

→ 완료한 퀘스트의 연관퀘스트를 '수행가능한 퀘스트 리스트'에 담아야 한다.

📝 4. 구현 코드

tree를 사용한 연계퀘스트에 중점을 맞췄기 때문에 다른 코드는 생략하였다.

- Quest 클래스

public abstract class Quest
{
    // 필드 
    protected string questName;   // 퀘스트이름
    protected string questStory;  // 퀘스트 스토리 (ex) 미니언들이 너무 많아졋다고 생각~
    protected int rewardGold;               // 리워드 
    protected string questPerform;          // 수행내역 
    protected QuestState questState;        // 수행 스탯 

    // 컨테이너
    protected Dictionary<Item, int> rewardItemByCount;        // 보상 아이템별 count
    protected Func<bool> completeQuest;                       // 성공여부 Func 

    // 트리구조
    protected Quest parentQuest;
    protected List<Quest> childQuest;

    // 프로퍼티
    public Quest ParentQuest { get => parentQuest; set { parentQuest = value; } }
    public List<Quest> ChildQuest => childQuest;

    public Quest(string name, string tooltip, string questPerfom , int rewardGold)
    {
        this.questName = name;
        this.questStory = tooltip;
        this.questPerform = questPerfom;
        this.questState = QuestState.beforeReceive;     // 생성할 때, 받기전으로 설정 
        this.rewardGold = rewardGold;

        if (rewardItemByCount == null)
            rewardItemByCount = new Dictionary<Item, int>();
        if(childQuest == null)
            childQuest = new List<Quest>(); 
    }

    public void AddToItem(Item item, int count)
    {
        // 코드생략
    }

    public void CheckState() 
    {
        // 받기전이면 return 
        if (questState == QuestState.beforeReceive)
            return;

        // 조건을 만족하면
        if (completeQuest.Invoke()) 
        {
            // 완료 state로 변환 
            this.questState = QuestState.complete;
        }
    }

    // child 자식 대입
    public void AddChild(Quest child) 
    {
        child.parentQuest = this;
        childQuest.Add(child);
    }
        // 하위에서 작성해야할 퀘스트 성공 조건
    public abstract bool TodoMission();

    // 퀘스트 진행내역
    public abstract string QuestProgress();
}

Quest 클래스

          ■ 자세한 설명은 상단의 클래스다이어그램 참고

           AddChild() 메서드

                        Quest클래스를 매개변수로 가진다

                        매개변수로 들어온 자식 퀘스트의 부모퀘스트를 this, 즉 내 클래스로 지정한다

                        현재 퀘스트의 자식클래스의 리스트에 매개변수로 들어온 자식 클래스를 add 한다

            CheckState() 메서드

                       Func<bool> completeQuest 를 Invoke()    

                                 ■ true를 return → complete로 상태 변환  

                                  flase를 return 상태 변환 x 

 

- MonsterKillQuest 클래스

public class MonsterKillQuest : Quest
{
    // 처치해야할 몬스터 
    // Monster 타입이면 더 좋을듯
    private List<string> monsterNameList;
    private List<int> killCountList;
    
    // 프로퍼티
    public List<string> MonsterNameList => monsterNameList;
    public List<int> KillCountList => killCountList;  

    public MonsterKillQuest(string name, string tooltip,string perForm , int rewardGold) : 
        base(name, tooltip , perForm , rewardGold)
    {
        // Func에 연결
        completeQuest += TodoMission;
    }

    public void AddtoKillMonsterList(string name , int cnt ) 
    {
        // 코드생략
    }

    public override bool TodoMission()
    {
        bool flag = true;

        // 지금까지 처치한 몬스터 수 
        for (int i = 0; i < monsterNameList.Count; i++) 
        {
            string nowMonster = monsterNameList[i];
            int countToKill = killCountList[i];

            // 잡은횟수
            int killCnt = DungeonManager.Instance.monsterCatches[nowMonster];

            // 처치한 몬스터가 kill count보다 낮으면 -> 실패
            if (killCnt < countToKill)
            { 
                flag = false;
                break;
            }
        }

        // for문안의 조건문에 걸리지않으면 -> killcount대로 다 잡은것 
        return flag;
    }

    public override string QuestProgress()
    {
        // 코드생략
    }

}

MonsterKillQuest 클래스

           자세한 설명은 상단의 클래스다이어그램 참고

            TodoMission() 메서드 오버라이딩 

                       퀘스트 완료 조건 구현

                      □ '현재 잡은 몬스터 수'가 '잡아야 하는 몬스터 수'보다 작으면 false를 return, 완료하면 true를 return

            생성자 

                      Func<bool> completeQuest 에 구현한 TodoMission() 메서드를 추가

 

- QuestManger 클래스

- 필드

private Quest currQuest;                // 현재 퀘스트 저장 

private List<Quest> performableQuests;  // 수행가능 퀘스트

currQuest

           현재 퀘스트 저장 

List<Quest> performableQuests

           수행가능한 퀘스트 컨테이너

           화면에 출력 할 때 해당리스트에 접근해서 퀘스트 이름을 출력

 

- Enter() 메서드

public void Enter()
{
    Console.Clear();

    // 수행가능한 퀘스트 이름만 빼서 배열로 저장 (LINQ)
    // 코드 생략
   
    // 목록(리스트) 출력
    // 코드 생략

    // player input 
    // 코드 생략

    // 1. 현재 퀘스트 
    currQuest = performableQuests[input - 1];
    // 2. 완료여부에 따라 state변화 
    currQuest.CheckState();

    // 퀘스트 print
    // 코드 생략

    // 성공여부에 따라 print 다름 
    PrintRewardBystate();
}

1. 플레이어에 입력에 맞게 Quest 타입의 currQuest를 저장한다.

2. currQuest의 상태변환 메서드를 실행한다 

           자세한 설명은 Quest 클래스의 CheskState() 메서드 참고

 

- PrintRewardBystate() 메서드

private void PrintRewardBystate()
{
    switch (currQuest.QuestState)
    {
        case QuestState.beforeReceive:
            // 코드생략
            break;
        case QuestState.afterReceive:
            // 코드생략
            break;
        case QuestState.complete:
            RemoveListAndAddToChild();
            break;
    }
}

1. 현재 퀘스트의 상태에 따라 다른 메서드를 실행한다.

2. 퀘스트가 complete 상태이면 (완료했으면) RemoveListAndAddToChild() 메서드를 실행한다. 

 

- RemoveAndAddToChild() 메서드

private void RemoveListAndAddToChild()
{
    // 현재 퀘스트의 state를 done으로
    currQuest.ChangeState(QuestState.done);

    // currQuest와 같은 quest 반환
    var temp = performableQuests.Find(quest => quest.Equals(currQuest));

    // 수행가능 리스트에서 삭제
    if (temp != null)
    {
        performableQuests.Remove(temp);
    }

    // 현재 퀘스트의 child리스트에 접근해서 가능한 퀘스트리스트에 넣어야 함 
    for (int i = 0; i < currQuest.ChildQuest.Count; i++)
    {
        performableQuests.Add(currQuest.ChildQuest[i]);
    }
}

1. 현재 퀘스트의 상태를 QuestState.done으로 변경한다.

2. 수행가능한 퀘스트를 담아놓는 리스트에서 현재 퀘스트를 remove 한다. 

3. 현재 퀘스트의 List<Quest>에 접근해서 수행가능한 퀘스트 리스트(perforambleQuest)에 추가한다.


📝 동작 영상

 

 


📝 마무리

: 현재는 수락불가능한 퀘스트가 출력되고 있지는 않다. 

유니티 개인 프로젝트를 할 때 수락 불가능한 퀘스트도 출력해서 퀘스트의 부모 퀘스트 추적 기능까지 추가해 보겠다.

📝 부모퀘스트 추적

(추가예정입니다)

 


TextRpg 팀 프로젝트 깃허브 주소 : 

https://github.com/kimYouChae/Window11_TextRPG

 

GitHub - kimYouChae/Window11_TextRPG

Contribute to kimYouChae/Window11_TextRPG development by creating an account on GitHub.

github.com

 

 

 

1. LINQ
    □ 개념
    □ 장/단점
2. 쿼리구문
3. 메서드 구문
4. 혼합된 쿼리 / 메서드 구문 
5. 직접 사용 예시

0. LINQ에 관한 다른 글

 

📝 1. LINQ 

□ Language-Integrated Query의 약자

             직역하면 "언어 통합 쿼리"

□ 관계형 데이터나 XML 데이터뿐만 아니라 모든 c# 데이터 컬렉션에 적용되는 일관된 쿼리  

 

🔖  장점

1. 데이터베이스, XML , 모든 c# 데이터 컬렉션에 동일한 쿼리 사용가능

2. 쿼리에서 사용되는 데이터 타입이 컴파일 타임에 체크됨       

             런타임에서 발생할 수 있는 오류를 줄임 

3. 데이터 처리 관련 로직을 간결하게 만들어줌

4. 데이터 필터링, 정렬, 집계, 조인 등의 작업을 간편하게 처리가능

 

🔖  단점

1. 복잡한 쿼리에서는 가독성이 떨어질 수도 있음

2. 대량의 데이터 처리 시 성능저하가 발생할 수도 있음

 

📝 2. Query Syntax ( 쿼리 구문 ) 형식

 □ from : 어느 데이터에서 가져올 것인지    

            num : 쿼리문 안에서 사용할 데이터 이름

□ where : 쿼리문에서 필터링할 조건

□ select : 조건에 만족하는 반환할 데이터 

 

예시

int[] numbers = [ 0, 1, 2, 3, 4, 5, 6 ];

// numQuery 는 IEnumerable<int> 타입
var numQuery = from num in numbers
               where (num % 2) == 0
               select num;

□ from :  int형 배열 numbers에서 가져옴

□ where : 배열 안의 값이 2로 나누어 떨어지면 (즉 짝수이면)

□ select : 값을 리턴한다

출력 [0, 2, 4, 6]

 

📝 3. Method Syntax ( 메서드 구문 ) 형식

: Sum, Max, Min, Average 등과 같은 함수를 사용한 쿼리 구문

 

예시

List<int> numbers1 = [ 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 ];
List<int> numbers2 = [ 15, 14, 11, 13, 19, 18, 16, 17, 12, 10 ];

// 메서드 구문 1
double average = numbers1.Average();

// 메서드 구문 2
IEnumerable<int> concatenationQuery = numbers1.Concat(numbers2);

□ 메서드 구문 1

            number1라는 int 타입 리스트에서의 평균값을 구한 후 return

□ 메서드 구문 2

            numbers1 리스트와 numbers2를 합침. 

            두 배열이 합쳐진 새로운 배열을 return 

 

📝 4. 혼합된 쿼리 / 메서드 구문 

var numCount1 = (
    from num in numbers1
    where num is > 3 and < 7
    select num
).Count();

□ 쿼리구문

           ■ numbers1 리스트에서 3 초과 7 미만의 값을 고른 후 select

□ 메서드 구문 

           ■반환된 IEnumerable<int 타입의 Count를 return

 

🔖  공식 c# 홈페이지에서 추천하는 방법

IEnumerable<int> numbersQuery =
    from num in numbers1
    where num is > 3 and < 7
    select num;

var numCount2 = numbersQuery.Count();

  쿼리 구문을 사용한 후

 새 변수를 할당해서 IEnumerable<intIEnumerable <int>의 Count를 return

 

📝 5. 직접 사용 예시

내일배움캠프 c# 프로그래밍-TextRpg-팀 프로젝트 중에 사용한 코드입니다.


🔖
 람다식을 사용한 메서드 구문 1

int count = InventoryManager.instance.mountableItems
    .Count(item => item.Type == questItemType && item.Equip);

  인벤토리 매니저의 List<MountableItems> 리스트에 접근

  Linq의 메서드 구문인 Count() 사용

            조건

                       : 아이템이 type이 questItemType과 같고

                       : 착용 여부를 나타내는 Equip변수가 true인 데이터

  결과

           ■ List<Mountable> mountablesItems 리스트에 들어있는 아이템 중에

            조건에 만족하는 아이템 개수를 알 수 있음.

 

🔖 람다식을 사용한 메서드 구문 2

public class Quest
{
    private string questName;   // 퀘스트이름
    
    // 프로퍼티
    public string QuestName => questName;
    
    public Quest(string name)
    {
        this.questName = name;
    }
}
private List<Quest> quests;     // 퀘스트 컨테이너 
private string[] questName;     // 퀘스트 이름 목록

quests.Add(new Quest(name: "마을을 위협하는 미니언 처치");
quests.Add(new Quest(name: "장비를 장착해보자");
quests.Add(new Quest(name: "더욱 더 강해지기!");

// quest에서 이름만 빼서 배열로 저장 (LINQ)
questOption = quests.Select(q => q.QuestName).ToArray();

Quest 클래스 인스턴스화 한 뒤 List<Quest>에 추가 

List<Quest>에서 QuestName을 select 해서 배열로 변환

  결과

            questName[0] : 마을을 위협하는 미니언처치

            questName[1] : 장비를 장착해 보자

            questName[2] : 더욱더 강해지기 

            배열에 Quest 클래스의 제목이 차례대로 들어가게 됨

 


📝 LINQ에 관한 다른 글

추가예정  


LINQ란

https://learn.microsoft.com/ko-kr/dotnet/csharp/linq/

 

C#의 LINQ(Language-Integrated Query) - C#

C#의 LINQ(Language-Integrated Query) 소개

learn.microsoft.com

 

컴파일 타임 때 타입 체크 

https://learn.microsoft.com/ko-kr/dotnet/csharp/linq/get-started/type-relationships-in-linq-query-operations

 

LINQ 쿼리 작업의 형식 관계 - C#

LINQ 쿼리의 변수 형식이 서로 관련되는 방식에 대해 알아봅니다. LINQ 쿼리 작업은 데이터 원본, 쿼리 및 실행에서 강력하게 형식화됩니다.

learn.microsoft.com

 

쿼리 구문과 메서드 구문 

https://learn.microsoft.com/ko-kr/dotnet/csharp/linq/get-started/write-linq-queries

 

LINQ 쿼리 작성 - C#

C#에서 LINQ 쿼리를 작성하는 방법을 알아봅니다.

learn.microsoft.com

 

1. delegate
    □ 개념
    □ 예시

📝 delegate (대리자)

1. 개념

메서드에 대한 참조를 나타내는 형식

C++의 함수 포인터와 비슷하게 동작함!

           ■함수포인터?

           : 함수의 시작 주소를 가리키는 포인터

           : 함수를 간접적으로 호출할 수 있게 해주는 변수

 

참조

하나 이상의 메서드를 참조할 수 있다.

참조할 메서드는 같은 반환값과 같은 매개변수를 가져야 한다.

□ += 연산자로 메서드 추가, -= 연산자로 메서드 제거가 가능하다.

 

호출

 메서드를 직접 호출하지 않고 메서드를 참조하고 있는 델리게이트를 통해 호출한다.

 델리게이트를 실행하면 참조하고 있는 모든 메서드가 실행된다.

 

📝 예시 

// 델리게이트
public delegate void Attack(float damage);
public class AttackHandler
{
    public Attack OnTtack;

    public void AttackExcute(float damage)
    {
        OnTtack?.Invoke(damage);
    }

    public void AddToAttack(Attack attck) 
    {
        OnTtack += attck;
    }
}

public class Player
{
    public float hp;
    public float damage;

    public Player(float hp,float d) 
    {
        this.hp = hp;
        this.damage = d;
    }

    public void GetAttack(float damage) 
    {
        hp -= damage;
        Console.WriteLine($"{damage} 데미지를 얻었습니다.");
    }
}

public class SystemAlarm
{
    public void UpdateUi(float damage) 
    {
        Console.WriteLine($"player가 {damage} 를 입었습니다.");
    }
}
    static void Main(string[] args)
    {
        // 
        AttackHandler AKHandler = new AttackHandler();

        // 인스턴스 생성
        Player player = new Player(10f, 3f);
        SystemAlarm alarm = new SystemAlarm();

        AKHandler.AddToAttack(player.GetAttack);
        AKHandler.AddToAttack(alarm.UpdateUi);

        // 델리게이트 실행
        AKHandler.AttackExcute( 7f );
    }

Attack 델리게이트

           float 매개변수를 받는 메서드들을 참조하는 형식

            float 매개변수를 받는 Player와 SystemAlarm 메서드 등록

  AttackExcute 호출 시 두 메서드가 순차적으로 실행

 

출력


함수포인터

https://www.tcpschool.com/cpp/cpp_function_pointer

 

코딩교육 티씨피스쿨

4차산업혁명, 코딩교육, 소프트웨어교육, 코딩기초, SW코딩, 기초코딩부터 자바 파이썬 등

tcpschool.com

델리게이트

https://learn.microsoft.com/ko-kr/dotnet/csharp/programming-guide/delegates/using-delegates

 

대리자 사용 - C#

대리자를 사용하는 방법을 알아봅니다. 대리자는 메서드를 안전하게 캡슐화하는 개체 지향적이고 형식이 안전하며 보안이 유지되는 형식입니다.

learn.microsoft.com

 

목차
1.Stream / StreamWriter
2. 디렉터리 존재 여부
3. 디렉토리 생성 / 삭제
4. 파일 존재 여부
5. 파일 생성 / 삭제 
6. 파일 읽기 

 

📝 System.IO 네임스페이스

using System.IO;

: 파일 시스템 작업 ( 디렉터리 / 파일 생성, 삭제, 수정 )에 관한 기능 제공 

: 텍스트 파일 작업 ( 파일 데이터 읽기 쓰기 )에 관한 기능 제공 

 

→ 더 많은 Syatem.IO 네임스페이스 기능

https://learn.microsoft.com/ko-kr/dotnet/standard/io/common-i-o-tasks

 

공통적인 I/O 작업 - .NET

.NET의 System.IO 네임스페이스에 있는 클래스와 메서드를 사용하여 공통적인 파일 작업 및 공통적인 디렉터리 작업을 수행하는 방법을 알아봅니다.

learn.microsoft.com

 

📝 1. Stream / StreamWriter

□ Stream 클래스

    ■ 마이크로소프트의 홈페이지의 설명에는 " 바이트 시퀀스의 제네릭 뷰를 제공"라는 표현을 사용한다.

    ■ 바이트 시퀀스란?

          □ 연속된 바이트들의 나열 

           텍스트파일/이미지파일/음악파일 이 저장될 때 바이트들의 시퀀스들로 저장된다. 

    ■ 제네릭 뷰?

           데이터가 무엇이든(텍스트, 이미지, 음악 등) 모두 바이트로 보고 처리할 수 있게 해주는 제네릭을 사용한다.

 

예시 )

타입이 다른 파일을 다룰때, 동일한 steam을 사용한다. 

// 텍스트 파일을 다룰 때도 Stream 사용 가능
FileStream textFile = File.OpenRead("text.txt");

// 이미지 파일을 다룰 때도 동일한 Stream 사용 가능
FileStream imageFile = File.OpenRead("image.jpg");

 

□ TextWriter

    ■ 일련의 문자를 순차적으로 작성할 수 있는 작성기 ( 추상 클래스)

  StreamWriter

    ■ TextWriter을 상속받아 실제로 구현한 클래스 

    ■ 문자를 바이트로 변환해서 Stream에 쓰는 실제 작업을 수행 

 

📝 2. 디렉토리 존재 여부 

Directory.Exists(savepath)

□ Bool 값을 반환

    ■ 해당 경로에 디렉터리가 존재하면 → true

    ■ 해당 경로에 디렉토리가 존재하지 않으면 → false 

 

📝 사용

using System.IO;

internal class Program
{
    static void Main(string[] args)
    {
        string savePath = @"D:\SpartaCodingClub\TextRPGSaveFile\";
        string saveFileName = "SaveFile";

        // 해당 디렉토리 (폴더) 가 없으면 생성
        if (!Directory.Exists(savePath))
        {
            Console.WriteLine("세이브 파일이 있습니다. ");
        }
        else 
        {
            Console.WriteLine("세이브 파일이 없습니다. ");
        }    
        }
    }
}

 

📝 3. 디렉토리 생성 / 삭제

📝 생성

Directory.CreateDirectory(savePath);

□ DirectoryInfo값을 반환

    ■ 지정된 경로에서 디렉터리를 나타내는 개체

📝 삭제

diretoryInfo.Delete();

□ DirectoryInfo에 저장된 디렉터리를 삭제 

 

📝 사용

using System.IO;

internal class Program
{
    static void Main(string[] args)
    {
        string savePath = @"D:\SpartaCodingClub\TextRPGSaveFile\";
        string saveFileName = "SaveFile";

        // 디렉토리 생성 
        DirectoryInfo di = Directory.CreateDirectory(savePath);

        // 디렉토리 삭제
        di.Delete();
    }
}

 

📝 4. 파일 존재 여부

File.Exists(savePath + saveFileName)

□ Bool 값을 반환 

    ■ 해당 경로에 파일이 존재하면 → true

    ■ 해당 경로에 파일이 존재하지 않으면 → false 

 

📝 사용

    internal class Program
    {
        static void Main(string[] args)
        {
            string savePath = @"D:\SpartaCodingClub\TextRPGSaveFile\";
            string saveFileName = "SaveFile";

            if (File.Exists(path))
            {
                 Console.WriteLine("파일이 존재합니다.");
            }
        }
    }

 

📝 5. 파일 생성 / 삭제

📝 생성

File.CreateText(파일명이 포함된 경로)

 StreamWriter 을 반환

이미 해당경로에 파일이 있는 경우 해당 내용이 바뀐다. 

 

📝 삭제

File.Delete(savePath + saveFileName);

해당 경로에 있는 파일을 삭제

 

📝 사용 

    internal class Program
    {
        static void Main(string[] args)
        {
            string savePath = @"D:\SpartaCodingClub\TextRPGSaveFile\";
            string saveFileName = "SaveFile";

            // 저장경로에 생성
            using (StreamWriter sw = File.CreateText(savePath + saveFileName))
            {
                sw.WriteLine("Hello ");
                sw.WriteLine("I'm ");
                sw.WriteLine("a talking potato.");
            }

        }
    }

 

📝 5. 파일 읽기 

File.ReadAllText(파일명이 포함된 경로);

□ string을 반환

    ■  파일의 모든 텍스트를 포함하는 문자열

 

📝 사용 

internal class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello, World!");
        //GameManger.Instance.EnterGame();

        string savePath = @"D:\Window11_TextRPG\";
        string saveFileName = "TestFile";

        string desString = File.ReadAllText(savePath + saveFileName);
    }
}

 


📝 Json 타입의 문자열을 저장하고 불러오는 과정 

https://youcheachae.tistory.com/32

 

[c#][TextRPG] 2. NewtonJson으로 직렬화&역직렬화

목차1. 저장 시스템    □ 설계 2. 저장    □ NuGet     □ Newtonsoft.Json    □ Json    □ 직렬화 / 역직렬화    □ visual Studio에서 사용방법    □ 실제 사용 예시 📝 저장 시스템: nuget의 NewtonJ

youcheachae.tistory.com


 

📝 참고한 링크 

https://learn.microsoft.com/ko-kr/dotnet/api/system.io.directory.createdirectory?view=net-8.0

 

Directory.CreateDirectory 메서드 (System.IO)

지정된 경로에 모든 디렉터리를 만듭니다.

learn.microsoft.com

https://learn.microsoft.com/ko-kr/dotnet/api/system.io.file.createtext?view=net-9.0

 

File.CreateText(String) 메서드 (System.IO)

UTF-8로 인코딩된 텍스트를 쓰기 위해 파일을 만들거나 엽니다. 파일이 이미 있는 경우 해당 내용이 바뀝니다.

learn.microsoft.com

https://learn.microsoft.com/ko-kr/dotnet/api/system.io.stream?view=net-8.0

 

Stream 클래스 (System.IO)

바이트 시퀀스의 제네릭 뷰를 제공합니다. 추상 클래스입니다.

learn.microsoft.com

https://learn.microsoft.com/ko-kr/dotnet/api/system.io.streamreader.-ctor?view=net-8.0

 

StreamReader 생성자 (System.IO)

지정된 스트림에 대한 StreamReader 클래스의 새 인스턴스를 초기화합니다.

learn.microsoft.com

 


📝 깃허브

https://github.com/kimYouChae/SpartaCodingClub

 

GitHub - kimYouChae/SpartaCodingClub

Contribute to kimYouChae/SpartaCodingClub development by creating an account on GitHub.

github.com

 

목차
1. 저장 시스템
    □ 설계 
2. 저장
    □ NuGet 
    □ Newtonsoft.Json
    □ Json
    □ 직렬화 / 역직렬화
    □ visual Studio에서 사용방법
3. 실제 사용 예시

 

📝 1. 저장 시스템

: nuget의 NewtonJson을 사용하여 클래스를 직렬화 한 뒤 텍스트파일로 저장

: 저장된 텍스트 파일을 불러와서 역직렬화 

 

1. 설계

 

 

 

📝 2. 저장 

1. Nuget

: c# 저장방식을 검색하다 보면  nuget의 NewtonJson을 사용하는 글이 많이 보인다. nuget이 뭘까?

 

https://www.nuget.org/

 

NuGet Gallery | Home

The NuGet Gallery is the central package repository for NuGet, the package manager for .NET.

www.nuget.org

:  " NuGet is the package manager for. NET." 즉 닷넷의 패키지 매니저이다.

 

https://learn.microsoft.com/ko-kr/nuget/quickstart/install-and-use-a-package-in-visual-studio

 

빠른 시작: Visual Studio에서 NuGet 패키지 설치 및 사용(Windows에만 해당)

이 빠른 시작에서는 Windows용 Visual Studio 프로젝트에서 NuGet 패키지를 설치하고 사용하는 방법을 알아봅니다.

learn.microsoft.com

: NuGet 패키지에는 다른 개발자가 프로젝트에서 사용할 수 있도록 만든 재사용 가능한 코드가 포함되어 있다. 

 

2. Newtonsoft.Json

: JSON 관련 작업을 할 때 편리한 기능을 제공하는 JSON 라이브러리 

 

3.Json

📝 what is Json 

     Java Script Object Notation의약자 

    데이터를 저장하고 전송하기 위한 형식

📝Json Data

      이름/값 쌍( 키/값 쌍 )으로 구성된다 

       JSON에서는 키는 큰따옴표로 묶인 문자열이어야 한다

{
  "name": "John",
  "age": 30
}

 

→ 더 자세한 내용은 w3 school 홈페이지 참고!

https://www.w3schools.com/js/js_json_syntax.asp

 

W3Schools.com

W3Schools offers free online tutorials, references and exercises in all the major languages of the web. Covering popular subjects like HTML, CSS, JavaScript, Python, SQL, Java, and many, many more.

www.w3schools.com

 

4. 직렬화 / 역직렬화 

📝Serialization (직렬화)

  문자열/숫자/객체/배열/bool/null 등의 타입을 json 문자열로 변환하는 과정

  JsonConvert.SerializeObject 사용

Product product = new Product();
product.Name = "Apple";
product.Expiry = new DateTime(2008, 12, 28);
product.Sizes = new string[] { "Small" };

string json = JsonConvert.SerializeObject(product);

 

📝 Deserializaion (역직렬화)

  json 문자열을 문자열/숫자/객체/배열/bool/null 등의 타입으로 재변환하는 과정, 직렬화의 반대과정 

JsonConvert.DeserializeObject <T> 사용

string json = @"{
  'Name': 'Bad Boys',
  'ReleaseDate': '1995-4-7T00:00:00',
  'Genres': [
    'Action',
    'Comedy'
  ]
}";

Movie m = JsonConvert.DeserializeObject<Movie>(json);

 


5. visual Studio에서 사용방법

□ 솔루션 탐색기 우클릭 → Nuget 패키지 관리 선택

□ newton 검색 후 다운로드

 

📝 3. 실제 사용 

1. 직렬화 & 저장

 // 직렬화 된 string 반환
 private string SerializedObject() 
 {
     try 
     {
         PlayerSaveData data = new PlayerSaveData();
        
        // 직렬화
         string json = JsonConvert.SerializeObject(data);

         //Console.WriteLine(json);
         return json;
     }
     catch (Exception e) 
     {
         Console.WriteLine($"직렬화 중 오류 발생 : {e.Message}");
         return string.Empty;
     }
   
 }
public void SaveData() 
{
    Console.WriteLine("데이터를 저장합니다.");

    // 직렬화
    string jsonString = SerializedObject();

    // 해당 디렉토리 (폴더) 가 없으면 생성
    if (!Directory.Exists(savePath))
        Directory.CreateDirectory(savePath);

    // string을 textFile로 저장
    // 파일이 이미 있으면 덮어씀
    using (StreamWriter sw = File.CreateText(savePath + saveFileName))
    {
        sw.WriteLine(jsonString);
    }
}

 

2. 역직렬화 / 불러오기 

 public void LoadData() 
 {
     // 경로 + 파일명에 파일이 있으면 ?
     if (File.Exists(savePath + saveFileName))
     {
         Console.WriteLine(" 저장파일이 있습니다.");

         // 불러오기 
         string desString = File.ReadAllText(savePath + saveFileName);

         // 역 직렬화
         try
         {
             PlayerSaveData data = JsonConvert.DeserializeObject<PlayerSaveData>(desString);
         }

         catch (Exception e) { Console.WriteLine(e); }
     }
     else 
     {
         Console.WriteLine(" 저장파일이 없습니다.");
     }
 }

 


📝디렉토리/텍스트파일을 저장하고 불러오는 방법

https://youcheachae.tistory.com/33

 

[c#][TextRPG] 3. c#의 System.IO를 이용한 파일 시스템 작업

목차1.Stream / StreamWriter2. 디렉터리 존재 여부3. 디렉토리 생성 / 삭제4. 파일 존재 여부5. 파일 생성 / 삭제 6. 파일 읽기  📝 System.IO 네임스페이스using System.IO;: 파일 시스템 작업 ( 디렉터리 / 파

youcheachae.tistory.com


📝 깃허브

https://github.com/kimYouChae/SpartaCodingClub

 

GitHub - kimYouChae/SpartaCodingClub

Contribute to kimYouChae/SpartaCodingClub development by creating an account on GitHub.

github.com

 

목차
1. Scene 관리 시스템
    □ 의도
    □ 설계
2. Scene 관리 구현방법과 상세 설명
    □ IScene 인터페이스
    □ GameManager
    □ 전환 관리
    □ 실제 사용 예시
3. 싱글톤 시스템
    □ 의도
    □ 설계
    □ 개선사항
4. 싱글톤 구현방법과 상세설명

 

📝 Scene 관리 시스템

1. 의도

: 게임의 각 상태 (로비, 인벤토리, 상점 등)를 Scene으로 관리하고, Scene 간의 전환을 관리하기 위해서 

 

2. 설계

□ SceneType라는  Enum을 통해 게임 Scene 타입 정의

□ IScene 인터페이스를 통해 각 Scene을 공통된 타입으로 관리

□ 씬 전환을 GameManager 를 통해 중앙에서 관리

 

📝 Scene 관리 구현방법과 상세 설명 

1. IScene 인터페이스

interface IScene
{
    // 씬에 들어갔을 때 
    public void SceneEntry();

    // 씬에서 메인으로 동작할 로직
    public void SceneMainFlow();
}

: 각 Scene이 필수로 구현해야할 동작을 정의한다.

          □ SceneEntry()

                     ■ Scene에서 초기 화면 구성 등의 기능이 실행된다.

          □ SceneMainFlow()

                       각 Scene에서 필요한 플레이어 입력처리, 상태 업데이트, 화면 출력 등의 기능이 실행된다.

 

2. 📝 GameManager.cs

enum SceneType 
{
    LobbyScene,
    PlayerScene,
    InventoryScene,
    StoreScene,
    RestScene,
    DungeonScene
}

class GameManger : Singleton<GameManger>    
{
    // 씬 저장 리스트 
    private IScene[] gameScene;
    private IScene nextScene;
    private IScene preScene;

    public GameManger() 
    {
        int temp = Enum.GetNames(typeof(SceneType)).Length;
        gameScene = new IScene[temp];

        gameScene[(int)SceneType.LobbyScene]        = LobbyScene.Instance;
        gameScene[(int)SceneType.PlayerScene]       = PlayerManager.Instance;
        gameScene[(int)SceneType.InventoryScene]    = InventoryManger.Instance;
        gameScene[(int)SceneType.StoreScene]        = StoreManager.Instance;
        gameScene[(int)SceneType.RestScene]         = RestScene.Instance;
        gameScene[(int)SceneType.DungeonScene]      = DungeonScene.Instance;

        nextScene = gameScene[0];
    }
}

□  Scene관리를 위한 배열 

           ■ IScene 인터페이스 타입의 배열을 선언 -> 모든 Scene을 하나의 컨테이너에서 관리

            nextScene과 preScene 변수를 통해 Scene 전환 추적 가능 

□   Scene 등록

           ■ SceneType enum의 개수 만큼 배열 크기 동적할당

            enum 값을 인덱스로 활용하여 Scene 인스턴스를 매핑 

 

3. 씬 전환 관리

public void ChangeScene(SceneType type) 
{
    // 지금 씬 = 예전 씬으로
    preScene = nextScene;

    // 현재 씬 지정 
    nextScene = gameScene[(int)type];

    // 현재씬 실행 
    if (preScene != nextScene)
        nextScene.SceneEntry();

    nextScene.SceneMainFlow();
}

□ 전환 로직  

           ■  이전 Scene 기록을 통해 히스토리 관리             

           ■  SceneType enum의 인덱스에 해당하는 Scene을 현재 Scene으로 지정

□ Scene Entry() 실행

           ■  이전 씬과 다른 Scene Type의 Scene인 경우에만 SceneEntry()를 실행

           ■   불필요한 출력 방지 

□ Scene MainFlow() 실행 

           ■  해당 Scnen의 주요 기능 실행 

           ■  입력처리 / 상태 업데이트 / 화면 출력 등을 실행 

 

4. 실행 예시 

 

-📝 LobbyScene.cs

private void ChangeScene(int input) 
{  
    switch (input) 
    {
        case 0:
            // 플레이어 출력 
            PlayerManager.Instance.printPlayer();
            // 로비로 씬 전환 
            GameManger.Instance.ChangeScene(SceneType.LobbyScene);
            break;
        case 1:
            // 인벤토리로 씬 전환 
            GameManger.Instance.ChangeScene(SceneType.InventoryScene);
            break;
        case 2:
            // 상점으로 씬 전환 
            GameManger.Instance.ChangeScene(SceneType.StoreScene);
            break;
        case 3:
            // 던전 씬 전환
            GameManger.Instance.ChangeScene(SceneType.DungeonScene);
            break;
        case 4:
            // 휴식
            GameManger.Instance.ChangeScene(SceneType.RestScene);
            break;
        case 5:
            // 저장 
            GameManger.Instance.SaveData();
            break;
        default:
            Console.WriteLine("잘못된 접근입니다. 다시 로비로 돌아갑니다");

            // Lobby로 돌아가기
            GameManger.Instance.ChangeScene(SceneType.LobbyScene);
            break;

    }
}

□  ChangeScene(int input) 

           ■   사용자 입력(input)에 따라 적절한 Scene으로 이동


📝 Singleton 관리 

1. 의도

: 유니티 프로젝트를 하면서 싱글톤을 상위 클래스로 만들어서 관리한 적이 있는데 편리했던 기억이 있어서 c#에서도 편리할 거라고 예상했다. 

 

2. 설계

□ 제네릭을 사용하여 다양한 타입의 클래스를 생성할 수 있도록 선언

 

3. 개선사항

: 상속을 사용하지 않고 클래스에 static을 붙여 전역에서 사용하는 방법도 가능할 것 같다.

 

📝 Singleton관리 구현방법과 상세 설명 

 

1. 📝 Singleton<T>.cs

class Singleton<T>
    where T : class , new()
{
    private static T instance;

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

            return instance;
        }
    }

}

□ 제네릭 <T>

           ■  다양한 참조 타입의 인스턴스 생성이 가능

           ■  where 제약조건을 통해 <T>는 참조타입 class와 매개변수가 없는 생성자를 가진 클래스만 허용

□ Instance 프로퍼티

           ■  최초 호출 시 인스턴스를 생성하며, 이후에는 동일한 인스턴스를 반환 

 


📝 깃허브

https://github.com/kimYouChae/SpartaCodingClub

 

GitHub - kimYouChae/SpartaCodingClub

Contribute to kimYouChae/SpartaCodingClub development by creating an account on GitHub.

github.com

 


📝 추후

: 저장시스템

: 아이템 구조 or 관리 방법

Interface란 

- 무엇을 해야 하는가에 대한 정의

     - 어떻게 구현할지는 명시하지 않는다.

- 실제로 "어떻게 구현하는가"는 하위 클래스에서 구현.

 

인터페이스의 특징 

1. 접근 제한 한정자를 가질 수 없다. public으로만 선언 가능하다.

: private으로 선언할 수 없다. 

 

2. 필드(변수)를 선언할 수 없다.

 

3. 인터페이스에 선언되는 메서드는 구현부를 가지지 않는다.

internal interface Interface1
{

    public 반환형 메서드이름();
    public 반환형 메서드이름();

    public int myProperty { get; set; }
}

 

c# 8.0 버전은 조금 다르다 그냥 참고만 하자! 

더보기

c# 8.0 버전에는 인터페이스의 메서드가 구현부를 가질 수 있다.

internal interface Interface1
{

    public 반환형 메서드이름 ();

    반환형 메서드이름()
    {
        Console.WriteLine("기본 구현입니다.");
    }

    public int myProperty { get; set; }
}

 

 

4. 클래스는 여러 인터페이스를 상속받을 수 있다.

interface Outerface2 
{
    public void OuterfaceMethod();
}

// 인터페이스 두개를 상속받은 클래스 
public class abcClase : Interface1 , Outerface2
{
        
}

 

5. 상속받은 인터페이스의 메서드를 반드시 구현해야 한다.

: 구현하지 않으면 "인터페이스 멤버를 구현하지 않습니다"라는 오류가 뜬다.

 

6. 인터페이스를 구현한 클래스의 인스턴스는 모두 같은 인터페이스 타입이다.

internal interface Interface1
{

    public void TestFunction();
}

// 인터페이스를 상속받는 클래스 1
public class temp1 : Interface1
{
    public void TestFunction()
    {
        
    }
}

// 인터페이스를 상속받는 클래스 2 
public class temp2 : Interface1
{
    public void TestFunction()
    {

    }
}

 

    static void Main(string[] args)
    {
        Interface1[] inter = new Interface1[2];
        inter[0] = new temp1();
        inter[1] = new temp2();
    }

: temp1과 temp2 클래스는 모두 Interface1이라는 인터페이스를 상속받았으므로 같은 타입이다. 

 

7. 인터페이스 안에는 메서드뿐만 아니라 프로퍼티 / 인덱서 / 이벤트도 가능하다.

출처 : microsoft 공식 c# interface 문서

 

 

8. 인터페이스는 인스턴스를 만들 수 없다.

 

 


추상클래스 vs interface

  추상클래스 abstract class 인터페이스 interface
공통점  
메서드 오버라이딩 하위 클래스는 반드시 메서드를 오버라이딩 해야한다.
인스턴스 인스턴스화 ( ex)new 키워드 사용 ) 불가능 
차이점    
필드 가질 수 있다. 가지지 못한다.
상속 인터페이스와 클래스 모두 상속가능 인터페이스 밖에 상속받지 못함.
생성자 가질 수 있다. 가지지 못한다.
메서드 구현  메서드 구현가능 메서드 구현 불가능

 


인터페이스 명명 규칙 

: 인터페이스 이름은 대문자 "I"로 시작한다.

public interface IAttack
{
    void IAttackMethod();
}

public interface ITracking 
{
    void ITrackinMethod();
}

도움이 된 링크
<interface>

https://learn.microsoft.com/ko-kr/dotnet/csharp/language-reference/keywords/interface

 

interface 키워드 - C# reference

'인터페이스' 키워드를 사용하여 모든 구현 형식이 지원해야 하는 계약을 정의합니다. 인터페이스는 관련되지 않은 형식 집합 간에 공통 동작을 만드는 수단을 제공합니다.

learn.microsoft.com

 

<명명규칙>

https://learn.microsoft.com/ko-kr/dotnet/csharp/fundamentals/coding-style/identifier-names

 

식별자 이름 - 규칙 및 관례 - C#

C# 프로그래밍 언어의 유효한 식별자 이름에 대한 규칙을 알아봅니다. 또한 .NET 런타임 팀과 .NET 문서 팀에서 사용하는 일반적인 명명 규칙을 알아봅니다.

learn.microsoft.com

 

 

+ Recent posts