<회의 사항>

  1. 오류수정 및 클래스 정리
  2. 추가 구현
  3. 콘솔 꾸미기

 

  • 오류

- 몬스터 랜덤값을 불러왔을 때, 같은 몬스터 동시 타겟팅이 되는 현상.- 장비 착용 시 옵션이 반영되지 않는 현상.- 데미지를 입었을 때, 방어력이 적용되지 않는 현상.

 

몬스터의 경우, 리스트에 들어있는 몬스터를 불러왔을 때 객체를 새로 생성해주지 않아서 그런 것이었다. 생성자를 통해 몬스터 객체를 새로 생성해주었다.

public static List<Monster> RandomMonsters()
{
    List<Monster> monsters = new List<Monster>
    {
        new Monster("변이 쥐", 100, 100, 1, true),
        new Monster("다친 경비 견", 100, 100, 1, true),
        new Monster("실험체 태아", 1, 20, 1, true),
        new Monster("슬라임", 1, 10, 1, true)
    };

    List<Monster> selectedMonsters = new List<Monster>();//load list
    Random random = new Random();//random

    int maxMonsters = random.Next(1, monsters.Count); //반복문에 사용될 몬스터 생성 최댓값 랜덤 생성

    for (int i = 0; i < maxMonsters; i++)
    {
        Monster selectedMonster = monsters[random.Next(monsters.Count)];
        selectedMonsters.Add(new Monster(selectedMonster));
    }
    return selectedMonsters;
}

 

장비 옵션이 적용되지 않는 이유는 스탯창에서 더해주기만 하고 그 값을 전투 때 불러오지 않았다. 방어력 역시 스탯창에 구현만 해두고 값을 가져오지 않아서 그런 것이었다.

 

  • 콘솔 꾸미기
// 윈도우 콘솔창 크기 고정
Console.SetWindowSize(x, y);

// 커서 위치
Console.SetCursorPosition(10,10);

// 커서 없애기
Console.CursorVisible = false;

 

저번에 올렸던 콘솔 배경과 글씨 등의 색을 바꾸는 방법과 콘솔창 크기 고정 및 커서 관련 메서드를 통해 콘솔창을 꾸몄다.

게임 시작화면의 로고가 가운데 오도록 마진값과 텍스트길이로 로고가 가운데 오도록 한 뒤, 로고가 빨강, 노랑, 초록, 파랑, 마젠타 색 순으로 반복하여 변하도록 했다.

 

<마치며>

이번 프로젝트를 진행하며 클래스와 객체에 대한 이해도가 매우 낮다는 사실을 깨달았다.

공부할 때는 분명 이해한 것 같았는데 막상 남에게 설명하려하니 말문이 막혔다. 객체는 알면 알 수록 더 모르겠다. 이 부분은 주말에 좀 더 공부해보아야 할 것 같다.

// 일반 클래스
public class 사람 {
    // ====== 사람 개개인의 특징을 나타낼 수 있는 것 = 상태 = 변수 ======
    public string name;
    public int age;
    public List<아이템> inventory;
    // ==================================================================

    // ====== 사람 만드는 생성자 ======
    // 자...객체가 만들어질거야...매개변수들 초기화해서 준비해...
    public 사람(string name, int age) {
        this.name = name;
        this.age = age;
        inventory = new List<아이템>();
    }
    // ================================


    // ====== 사람이라면 할 수 있는 기능 = 함수 = 메소드 ======
    public void 숨쉬기() {
        // 대충 숨쉬는 코드;
    }
    public void 걷기() {
        // 대충 걷는 코드;
    }
    public void 달리기() {
        // 대충 달리는 코드;
    }
    // ========================================================
}

// 정적 클래스와 객체 생성
public static class 조물주 
{
	// 객체 생성
    public static List<사람> 세계사람목록 = new List<사람>();
    public static 사람 사람만들기(string name, int age) {
        사람 새로운사람 = new 사람(name, age);
        세계사람목록.Add(새로운사람);
        return 새로운사람;
    }
    public static void 사람죽이기(사람 죽일사람) {
        세계사람목록.Remove(죽일사람);
    }
    public static int 세계인구() {
        return 세계사람목록.Count;
    }
    public static void 타노스() {
        for (int i = 0; i < 세계사람목록.Count / 2; i++) {
            사람죽이기(세계사람목록[i]);
        }
    }
}

// 정적 클래스
public static class Game {
    // 게임을 실행하면 여기서부터 실행됩니다.
    public static void Main() {
        조물주.사람만들기("김철수", 20);
        조물주.사람만들기("박개발", 21);
        조물주.사람만들기("이새발", 22);
        Console.WriteLine("세계 인구: " + 조물주.세계인구());          // 콘솔에 "세계 인구: 3" 이 출력됨.
        조물주.사람만들기("박엘리자베스", 10232);
        조물주.타노스();
        Console.WriteLine("세계 인구: " + 조물주.세계인구());          // 콘솔에 "세계 인구: 2" 가 출력됨.

        조물주.세계사람목록[0].숨쉬기();
        조물주.세계사람목록[0].달리기();

    }
}
  1. 클래스는 상태와 동작을 가집니다.
  2. 예제에서는 사람의 개성을 나타내는 name, age, inventory가 상태입니다.
  3. 모든 사람이 공통적으로 가지는 동작인 숨쉬기(), 걷기(), 달리기()가 동작입니다.
  4. 클래스는 그 자체로 값이 되지 않고 이를 바탕으로 만들어진 객체가 실제 값이다.
  5. 예제에서는 조물주의 사람만들기 기능을 통해 네 명의 사람을 만듭니다.
  6. 사람 그 자체는 실존하는 값이 아니고, 김철수, 박개발, 이새발, 박엘리자베스가 실존하는 값입니다.
  7. 조물주는 단 하나만 존재해도 되는, 기능만을 담당하는 클래스 => 정적클래스
  8. Game에서 조물주의 개체를 생성하지는 않습니다. 클래스로부터 바로 함수를 참조할 수 있습니다.
  9. static class 내부에 있는 모든 변수, 함수는 static으로 선언됩니다.

출처 <김세진님> 예시 감사합니다...

 

 

Nullable 형

null은 아무것도 없다, 즉 참조를 하지 않았다는 뜻이다.

값형은 원래 null을 가질 수 없다. 그러나 값형에 Nullable을 달면 null이라고 하는 값을 사용할 수 있게 된다. 주로 값형 변수가 null인지 아닌지를 구분해야 하는 경우에 사용된다.

// Nullable 형식 변수 선언
int? nullableInt = null;
double? nullableDouble = 3.14;
bool? nullableBool = true;

// 값 할당 및 접근
nullableInt = 10;
int intValue = nullableInt.Value;

// null 값 검사
if (nullableDouble.HasValue)
{
    Console.WriteLine("nullableDouble 값: " + nullableDouble.Value);
}
else
{
    Console.WriteLine("nullableDouble은 null입니다.");
}

// null 병합 연산자 사용
// nullableInt ?? 0과 같이 사용되며, nullableInt가 null이면 0을 반환합니다.
int nonNullableInt = nullableInt ?? 0;
Console.WriteLine("nonNullableInt 값: " + nonNullableInt);

 

 

문자열 빌더(StringBuilder)
  • 문자열 조작

문자열들을 내부적인 버퍼에 넣어 놓고 받아서 조합만 해놨다가 필요할 때 문자열로 만들어준다.

Append(), Insert(), Replace(), Remove() 등 다양한 메서드를 치환할 수 있다.

  • 가변성

내부 버퍼를 사용하여 크기가 동적으로 변할 수 있다. 추가했다 뺐다 바꾸기도 가능하며 추가적인 메모리 할당을 해서 가변적으로 길이를 조절할 수 있다.

  • 효율적인 메모리 관리

주요 메서드

Append: 뒤에 문자열 추가

Insert: 지정한 위치에 문자열 삽입

Remove: 지정한 위치에서 문자열 제거

Replace: 문자열의 일부를 다른 문자열로 대체

Clear: StringBuilder의 내용을 모두 지운다.

StringBuilder sb = new StringBuilder();

// 문자열 추가
sb.Append("Hello");
sb.Append(" ");
sb.Append("World");

// 문자열 삽입
sb.Insert(5, ", ");

// 문자열 치환
sb.Replace("World", "C#");

// 문자열 삭제
sb.Remove(5, 2);

// 완성된 문자열 출력
string result = sb.ToString();
Console.WriteLine(result);

 

 

델리게이트(Delegate)

메서드를 참조하는 타입. 메서드를 변수에 저장할 수 있다.

 

델리게이트 구현

event는 할당(=)을 사용할 수 없고 +=나 -=만 사용할 수 있으며, 클래스 외부에서 직접 이벤트를 호출할 수 없다. 이를 통해 해당 코드의 보안성을 높이고 캡슐화 한다.

// 델리게이트 선언
public delegate void EnemyAttackHandler(float damage);

// 적 클래스
public class Enemy
{
    // 공격 이벤트
    public event EnemyAttackHandler OnAttack;

    // 적의 공격 메서드
    public void Attack(float damage)
    {
        // 이벤트 호출
        OnAttack?.Invoke(damage);
        // null 조건부 연산자
        // null 참조가 아닌 경우에만 멤버에 접근하거나 메서드를 호출
    }
}

// 플레이어 클래스
public class Player
{
    // 플레이어가 받은 데미지 처리 메서드
    public void HandleDamage(float damage)
    {
        // 플레이어의 체력 감소 등의 처리 로직
        Console.WriteLine("플레이어가 {0}의 데미지를 입었습니다.", damage);
    }
}

// 게임 실행
static void Main()
{
    // 적 객체 생성
    Enemy enemy = new Enemy();

    // 플레이어 객체 생성
    Player player = new Player();

    // 플레이어의 데미지 처리 메서드를 적의 공격 이벤트에 추가
    enemy.OnAttack += player.HandleDamage;

    // 적의 공격
    enemy.Attack(10.0f);
}

 

 

람다(Lambda)

이름이 없는 익명 메서드 중 하나. 참조를 통해 컨트롤 한다.

// 델리게이트 선언
public delegate void GameEvent();

// 이벤트 매니저 클래스
public class EventManager
{
    // 게임 시작 이벤트
    public event GameEvent OnGameStart;

    // 게임 종료 이벤트
    public event GameEvent OnGameEnd;

    // 게임 실행
    public void RunGame()
    {
        // 게임 시작 이벤트 호출
        OnGameStart?.Invoke();

        // 게임 실행 로직

        // 게임 종료 이벤트 호출
        OnGameEnd?.Invoke();
    }
}

// 게임 메시지 클래스
public class GameMessage
{
    public void ShowMessage(string message)
    {
        Console.WriteLine(message);
    }
}

// 게임 실행
static void Main()
{
    // 이벤트 매니저 객체 생성
    EventManager eventManager = new EventManager();

    // 게임 메시지 객체 생성
    GameMessage gameMessage = new GameMessage();

    // 게임 시작 이벤트에 람다 식으로 메시지 출력 동작 등록
    eventManager.OnGameStart += () => gameMessage.ShowMessage("게임이 시작됩니다.");

    // 게임 종료 이벤트에 람다 식으로 메시지 출력 동작 등록
    eventManager.OnGameEnd += () => gameMessage.ShowMessage("게임이 종료됩니다.");

    // 게임 실행
    eventManager.RunGame();
}

 

 

Func과 Action

Func과 Action은 미리 정의된 제네릭 형식이다.

Func은 반환값이 있는 델리게이트이고 Action은 반환값이 없이 실행만 되는 메서드이다. Func에 int와 string을 준다면 int는 매개변수이고 뒤의 마지막으로 나오는 string은 반환값이다. Action 같은 경우에는 매개변수가 있으면 반환값이 없기 때문에 둘다 매개변수이다.

// 게임 캐릭터 클래스
class GameCharacter
{
    private Action<float> healthChangedCallback;

    private float health;

    public float Health
    {
        get { return health; }
        set
        {
            health = value;
            healthChangedCallback?.Invoke(health);
        }
    }

    public void SetHealthChangedCallback(Action<float> callback)
    {
        healthChangedCallback = callback;
    }
}

// 게임 캐릭터 생성 및 상태 변경 감지
GameCharacter character = new GameCharacter();
character.SetHealthChangedCallback(health =>
{
    if (health <= 0)
    {
        Console.WriteLine("캐릭터 사망!"); // 코드가 한 줄일 경우 중괄호 생략 가능
    }
});

// 캐릭터의 체력 변경
character.Health = 0;

델리게이트와 동일하지만 이미 정의, 구현 되어 있기 때문에 안전하고 코드가 한결 간단해진다.

 

 

LINQ(Language Intergrated Query)

.NET 프레임워크에서 제공되는 쿼리 언어 확장. 코드에서 collection이나 배열이나 데이터베이스에 쿼리를 던질 수 있다.

 

구조

var result = from 변수 in 데이터소스
             [where 조건식]
             [orderby 정렬식 [, 정렬식...]]
             [select 식];
  • var : LINQ에서 쿼리를 전달받는 형식들이 다양할 수 있기 때문에 사용한다.
  • from : 변수를 꺼내온다.
  • where절
  • orderbt 정렬
  • select 
// 데이터 소스 정의 (컬렉션)
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

// 쿼리 작성 (선언적인 구문)
var evenNumbers = from num in numbers
                  where num % 2 == 0
                  select num;

// 쿼리 실행 및 결과 처리
foreach (var num in evenNumbers)
{
    Console.WriteLine(num);
}

 

 

 예외 처리

예외 처리는 프로그램이 갑작스럽게 종료 되거나 멈추는 상황을 예방하고 안정적으로 유지할 수 있도록 구현해준다.

 

예외 처리 구현

try에 예외가 발생할 수 있는 코드를 작성하고 catch에 예외 처리를 한다.

catch문은 위에서부터 순서대로 실행된다. 다중으로 붙일 수 있어 여러 가지 예외 처리를 할 수 있고, 예외 객체를 받아올 수 있어 예외 처리에 발생한 여러 정보들을 표시할 수 있다.

마지막으로 finally은 예외와 상관 없이 무조건 실행되는 코드이다. 필요하지 않다면 생략 가능하다.

try
{
    // 예외가 발생할 수 있는 코드
}
catch (ExceptionType1 ex)
{
    // ExceptionType1에 해당하는 예외 처리
}
catch (ExceptionType2 ex)
{
    // ExceptionType2에 해당하는 예외 처리
}
finally
{
    // 예외 발생 여부와 상관없이 항상 실행되는 코드
}

 

사용자 정의 예외

사용자가 필요에 따라 자신만의 예외 클래스를 작성할 수 있다.

Exception 클래스를 상속받아 작성한다.

public class NegativeNumberException : Exception
{
    public NegativeNumberException(string message) : base(message)
    {
    }
}
internal class Program
    {
    static void Main(string[] args)
    {
        try
        {
            int number = -10;
            if (number < 0)
            {
                throw new NegativeNumberException("음수는 처리할 수 없습니다.");
            }
        }
        catch (NegativeNumberException ex)
        {
            Console.WriteLine(ex.Message);
        }
        catch (Exception ex)
        {
            Console.WriteLine("예외가 발생했습니다: " + ex.Message);
        }
    }
}

 

예외 처리 할 때는 가능한 구체적인 예외 클래스를 사용하는 것이 코드가 안정적이고 예외 처리에 대한 처리가 더욱 정확해진다.

if else문으로 예외를 처리하면 되지 않나 생각이 들었었는데 이 때문에 예외 처리를 사용한다는 것을 깨달았다.

 

 

값형과 참조형

값형(Value Type)

변수에 값을 직접 저장한다. 변수가 독립적으로 데이터를 가지며 어딘가에 할당하거나 대입 해야할 때 값을 복사해서 준다.

예) int, float, double, bool 등

struct MyStruct
{
    public int Value;
}

MyStruct struct1 = new MyStruct();
struct1.Value = 10;

MyStruct struct2 = struct1; // struct2는 struct1의 값 복사

struct2.Value = 20;

Console.WriteLine(struct1.Value); // 출력 결과: 10

 

 

참조형(Reference Type)

변수에 데이터에 대한 참조(메모리 주소)를 저장한다. 변수가 동일한 데이터를 참조하여 할당하거나 대입 해야할 때 참조를 복사해서 준다.

예) 클래스, 배열, 인터페이스 등

class MyClass
{
    public int Value;
}

MyClass obj1 = new MyClass();
obj1.Value = 10;

MyClass obj2 = obj1; // obj2는 obj1과 동일한 객체를 참조

obj2.Value = 20;

Console.WriteLine(obj1.Value); // 출력 결과: 20

 

 

박싱과 언박싱

박싱은 값형을 참조형으로 변환한다. 언박싱은 박싱으로 참조형이었던 값형을 다시 값형으로 돌려놓는다.

 

object 형식은 직,간접적으로 모든 클래스의 최상위 클래스이다.

// 값형
int x = 10;
int y = x;
y = 20;
Console.WriteLine("x: " + x); // 출력 결과: 10
Console.WriteLine("y: " + y); // 출력 결과: 20

// 참조형
int[] arr1 = new int[] { 1, 2, 3 };
int[] arr2 = arr1;
arr2[0] = 4;
Console.WriteLine("arr1[0]: " + arr1[0]); // 출력 결과: 4
Console.WriteLine("arr2[0]: " + arr2[0]); // 출력 결과: 4

// 박싱과 언박싱
int num1 = 10;
object obj = num1; // 박싱
int num2 = (int)obj; // 언박싱
Console.WriteLine("num1: " + num1); // 출력 결과: 10
Console.WriteLine("num2: " + num2); // 출력 결과: 10
List<object> myList = new List<object>();

// 박싱: 값 형식을 참조 형식으로 변환하여 리스트에 추가
int intValue = 10;
myList.Add(intValue); // int를 object로 박싱하여 추가

float floatValue = 3.14f;
myList.Add(floatValue); // float를 object로 박싱하여 추가

// 언박싱: 참조 형식을 값 형식으로 변환하여 사용
int value1 = (int)myList[0]; // object를 int로 언박싱
float value2 = (float)myList[1]; // object를 float로 언박싱

값형을 참조형으로 변환할 때, 값형이 사라지는 것이 아니라 남아있고 값형에 대한 참조형으로 변환되어 새롭게 받아지는 것이다. 그렇기 때문에 너무 많은 박싱과 언박싱이 일어나면 성능이 저하된다.

 

값과 참조형, 박싱과 언박싱은 자주 사용되기 때문에 잘 알아두는 것이 좋을 것 같다.

+ Recent posts