250101

lililllilillll·2025년 1월 1일

개발 일지

목록 보기
38/350

✅ 오늘 한 일


  • Project Etude
  • C# 교과서 읽기


📝 배운 것들


🏷️ Unity :: New Input System

세팅 방법

Package Manager > Input System 설치 > Yes로 재부팅
Edit > Project Settings > Player > Other Settings > Active Input Handling > Both로 돼있는지 확인 (Input System Package (New)로 바꿔도 됨)

다양한 컨트롤러들의 입력을 Input Actions라는 형태로 포장한 Input Action Asset이 있다.
이 Asset을 Player Action이라는 컴포넌트가 읽어서 Input Event를 발동시키는 것.

No Control Schemes > Add control scheme > 어떤 제어 장치를 대상으로 할지 리스트에 추가 > Save
하나의 Action Map에는 여러 Input Action들이 있다.
Action들마다 물리적인 Key들이 binding되어야 함.
이를 위해 Path에서 할당 가능 (Listen을 통해 키 입력하면 바로 할당 가능)

Action Properties

Action Type

  • Value : 지속적으로 값을 변화하고자 할 때
  • Button : 단일하게 버튼 누르고 떼는 Action에 적합
  • Pass through : 동일한 Action Input에 바인딩된 모든 장치들의 입력을 모두 트리거로 발생시킴

Initial State Check

Player Input이 활성화 될 때 키가 중립으로 되어있는지 확인

Interactions

  • Hold : 지정한 시간만큼 누르고 있어야 입력으로 인정됨
  • Tap : 지정한 시간 안에 눌렀다 떼야 함

Processors

입력을 편집해주는 기능 (출력값의 크기를 바꾸거나 반대로 반전)

🏷️ C# :: ToString()

ToString()은 객체를 문자열로 변환하는 표준 메서드입니다.

  • 기본적으로 클래스 이름을 반환하지만, 이를 오버라이드하면 객체의 의미 있는 표현을 반환할 수 있습니다.
  • Console.WriteLine() 같은 메서드에서 자동으로 호출되기 때문에, 객체를 출력할 때 유용하게 사용됩니다.

ToString()을 재정의하면 다음과 같은 경우에 객체의 의미 있는 문자열 표현을 제공할 수 있습니다:

  • 디버깅 시 객체의 상태를 확인.
  • 로그 파일에 객체 정보를 기록.
  • 객체를 사람 친화적으로 출력.

예를 들어:

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public override string ToString()
    {
        return $"{Name}, {Age} years old";
    }
}

Person p = new Person { Name = "Alice", Age = 30 };
Console.WriteLine(p); // "Alice, 30 years old"


🎮 Project Etude


일시정지 기능

    /// <summary>
    /// 게임 시작하면 음악 재생. 음악 재생중이라면 이전과의 간격 계산.
    /// </summary>
    private void CheckMusic()
    {
        if (game_ongoing)
        {
            if (!MainMusic.isPlaying)
            {
                MainMusic.Play();
                lastBlockStartTime = AudioSettings.dspTime;
            }
            else
            {
                delta_dspTime = AudioSettings.dspTime - lastBlockStartTime;
                // DebugText.text = "delta_dspTime : " + delta_dspTime;
            }
        }
    }

    /// <summary>
    /// 판정용 플레이어 이동
    /// </summary>
    private void player_move()
    {
        // nowBlockPos와 nextBlockPos를 포함하는 1차 함수를 만들어서, delta_dspTime에 따른 플레이어 위치를 계산
        float newX = (float)(nowBlockPos.x + (nextBlockPos.x - nowBlockPos.x) * delta_dspTime / storedNoteDuration);
        float newY = (float)(nowBlockPos.y + (nextBlockPos.y - nowBlockPos.y) * delta_dspTime / storedNoteDuration);
        transform.position = new Vector3(newX, newY, transform.position.z);
    }
    
    /// <summary>
    /// 판정 성공했을 때 경로선의 기준들을 업데이트한다.
    /// </summary>
    private void UpdateLineBasis()
    {
        // 경로의 기준 블럭 위치들을 갱신한다
        nowBlockPos = missionBlockCollider.transform.position;
        if (missionBlockScript.nextNoteBlock != null)
            nextBlockPos = missionBlockScript.nextNoteBlock.transform.position;

        // 경로의 계산 기준점을 노트 길이만큼 이동시킨다
        lastBlockStartTime += storedNoteDuration;
        storedNoteDuration = missionBlockScript.noteDuration * (60 / bpm);
    }

현재 이동 관련 코드들

처음 시작하면 lastBlockStartTime을 AudioSettings.dspTime으로 초기화한다.
판정이 성공하면 storedNoteDuration만큼 이동시킨다.
storedNoteDuration은 0으로 초기화돼있기 때문에 맨 처음 시작할 땐 0을 더한다
이후 판정이 성공할 때마다 이번에 판정 성공했던 missionBlock의 noteDuration을 가져와 (60/bpm)을 곱해서 실제 시간으로 바꿔준다.

AudioSettings.dspTime은 씬이 시작했을 때부터 현재까지의 시간을 측정한 값이다
거기에 lastBlockStartTime을 빼준 delta_dspTime으로
플레이어가 마지막 블럭의 곡 시간초에서부터 상대적으로 얼마나 진행되어 있는지를 나타낼 수 있다.

    public void ResumeGame()
    {
        game_paused = false;
        MainMusic.UnPause();
        pauseStartTime = AudioSettings.dspTime;
    }

    public void PauseGame()
    {
        game_paused = true;
        MainMusic.Pause();
        pausedDuration += AudioSettings.dspTime - pauseStartTime;
    }

esc를 누르면 PauseGame()이, Resume 버튼을 누르면 ResumeGame()이 호출될 것이다.

player_move()는 delta_dspTime을 기반으로 이동하므로,
멈춰져있던 시간만큼 delta_dspTime이 왜곡되지 않도록 보상해줘야 한다.

이를 위해서 pausedDuration을 기록해두고,
delta_dspTime에서 빼줘야 한다.

delta_dspTime = AudioSettings.dspTime - lastBlockStartTime - pausedDuration;

디버그 2가지

  • pauseStartTime과 pausedDuration의 계산 위치가 서로 바뀜
  • 게임 오버되면 apusedDuration 초기화해줘야됨

esc 누르면 pause, esc 한 번 더 누르거나 resume 버튼 누르면 resume을 연결한 이후 버그 발생

디버그

  • game_ongoing 전에 esc 누르면 (pause를 호출하면) resume시 offset 반영되어 뒤로 가버림 → game_ongoing if 문 추가
  • game_ongoing 전에 resume을 호출하면 음악이 시작되어버림 → 위와 함께 if문에 추가

구현 완료

앞으로 해야될 것들

  • 히트 이펙트
  • 처음 시작할 때 곡 소개
  • Perfect, Good, Nice 등 거리별 판정 및 이펙트
  • json 파일 기반으로 맵 로딩하도록 리팩토링
  • 해상도 변경
  • 맵 아트 폴리싱
  • 씬 전환 효과
  • 메인 화면 (스테이지 선택 및 옵션 선택)
  • 튜토리얼
  • 맵 추가
  • 맵 에디터 편의성 증가시키기
  • 맵 에디터에 블럭에 해당하는 카메라 연출, 히트 이펙트 등 옵션 넣을 수 있는 기능 추가
  • 블루투스 이어폰 싱크 문제 해결


📖 C# 교과서


43 상속으로 클래스 확장하기

클래스 간에 부모와 자식 관계를 설정하는 것을 개체 관계 프로그래밍이라 한다.
상속은 부모 클래스에 정의된 내용을 다시 사용하거나 확장 또는 수정하여 자식 클래스로 만드는 것.

43.1 클래스 상속하기

상속 : 부모 클래스의 모든 멤버를 자식 클래스가 재사용하도록 허가하는 기능
C#은 단일 상속만 지원함. 다중 상속은 인터페이스로만 할 수 있음.

클래스 상속 구문

public class 기본클래스이름
{
	// 기본 클래스의 멤버 정의
}

public class 파생클래스이름 : 기본클래스이름
{
	// 기본 클래스의 멤버를 포함한 자식 클래스의 멤버 정의
}
  • System.Object 클래스 : 모든 클래스의 부모 클래스. 모든 클래스는 자동으로 Object에서 상속받는다. (상속받는 코드는 생략됨)
  • 기본(base) 클래스 : 다른 클래스의 부모 클래스가 되는 클래스. Base 클래스, Super 클래스라고도 한다.
  • 파생(derived) 클래스 : 다른 클래스의 자식 클래스가 되는 클래스. Derived 클래스, Sub 클래스, 자식 클래스라고도 한다.

43.2 부모 클래스와 자식 클래스

public, protected로 선언된 멤버들은 자식 클래스에서 사용 가능

using System;

namespace InheritanceDemo
{
    class Parent
    {
        public void Foo() => Console.WriteLine("부모 클래스의 멤버 호출");
    }

    class Child : Parent
    {
        public void Bar() => Console.WriteLine("자식 클래스의 멤버 호출");
    }

    class InheritanceDemo
    {
        static void Main()
        {
            var child = new Child();
            child.Foo();
            child.Bar();
        }
    }
}

43.5 부모 클래스 형식 변수에 자식 클래스의 개체 할당하기

> class Developer
. {
.     public override string ToString()
.     {
.         return "developer";
.     }
. }
> class WebDeveloper : Developer
. {
.     public override string ToString() => "Web dev";
. }
> 
> class MobileDeveloper : Developer
. {
.     public override string ToString() => "mobile dev";
. }
> 
> var web = new WebDeveloper();
> Console.WriteLine(web);
Web dev
> var mobile = new MobileDeveloper();
> Console.WriteLine(mobile);
mobile dev

43.7 this와 this(), base와 base()

클래스 내에서
this는 자신을 의미하고, this()는 자신의 생성자를 나타낸다.
base는 부모 클래스를 의미하고, base()는 부모 클래스의 생성자를 나타낸다.

base 키워드를 사용하여 부모 클래스의 생성자 호출하기

자식 클래스 생성자에서 바로 어떤 일 처리하는게 아니라 부모 클래스의 생성자에 전달할 때가 있음.
이때 자식 클래스의 생성자에서 콜론 기호 뒤에 base()를 사용하여 부모 클래스의 생성자를 호출함.

namespace ConstructorBase
{
    class Parent
    {
        public Parent(string message) => Console.WriteLine(message);
    }
    class Child : Parent
    {
        public Child(string message) : base(message) { }
    }
    class ConstructorBase
    {
        static void Main()
        {
            string message = "자식 클래스의 생성자를 호출할 때 부모 클래스의 생성자로 전달";
            var child = new Child(message);
        }
    }
}

초기화할 때 부모 생성자 가져오는거

생성자의 특징

클래스와 이름이 같다
생성자의 이름은 클래스의 이름과 동일해야 합니다.

반환형이 없다
생성자는 반환형을 명시하지 않으며, 반환값도 없습니다. 예를 들어, void 키워드조차 사용하지 않습니다.

자동 호출
객체를 new 키워드로 생성할 때 자동으로 호출됩니다.

기본 생성자(Default Constructor)
매개변수가 없는 생성자는 기본 생성자라고 하며, 클래스를 정의할 때 생성자를 명시하지 않으면 컴파일러가 기본 생성자를 자동으로 제공합니다.

namespace BaseKeyword
{
    public class Car
    {
        private string name;
        public Car(string name)
        {
            this.name = name;
        }
        public void Run() => Console.WriteLine($"{this.name}가 달린다.");
    }

    public class My : Car
    {
        public My() : this("나의 자동차") { }
        public My(string name) : base(name) { }
    }
    public class Your : Car
    {
        public Your() : base("너의 자동차") { }
    }

    class BaseKeyword
    {
        static void Main()
        {
            (new My()).Run();
            (new My("나의 끝내주는 자동차")).Run();
            new Your().Run();
        }
    }
}

My의 생성자 2개 : 매개변수 없는 친구가 매개변수 있는 친구를 호출
(new My()).Run()에 있는 괄호는 없어도 됨

43.8 봉인 클래스

봉인(sealed) 클래스 : 더 이상 다른 클래스에 상속되지 않게 사용하는 클래스. 최종 클래스라고도 하며, 선언부에 sealed 키워드를 붙여 만든다.

sealed partial class App : Application

43.9 추상 클래스

클래스를 선언할 때 추가로 abstract 키워드를 붙여 클래스를 선언할 수 있다. 이를 추상(abstract) 클래스라 한다.
추상 클래스는 다른 클래스의 부모 클래스 역할을 한다

public abstract class AbstractClassDemo

추상 클래스를 사용하여 부모 클래스 만들기

추상 클래스는 일반적인 클래스들의 부모 역할을 하는 클래스, 즉 공통 기능들을 모아 놓은 클래스 역할을 한다.

  • 추상 클래스로 개체 못 만든다.
  • 클래스 설계할 때 멤버 이름 통일할 때 쓴다.
  • 상속 준 후에 하위 클래스에서 추가 기능 직접 구현해야 하는 강제성을 띤다
> public abstract class TableBase
. { 
.     public int Id { get; set; }
.     public bool Active { get; set; }
. }
> TableBase tableBase = new TableBase();
(1,23): error CS0144: 추상 형식 또는 인터페이스 'TableBase'의 인스턴스를 만들 수 없습니다.
> public class Children : TableBase
. { 
.     public string Name { get; set; }
. }
> var child = new Children() { Id = 1, Active = true, Name = "child" };
> if (child.Active)
. {
.     Console.WriteLine($"{child.Id} - {child.Name}");
. }
1 - child

추상 클래스를 만들고 상속하기

> public class Square : Shape
. {
.     private int _size;
.     public Square(int size)
.     {
.         _size = size;
.     }
. 
.     public override double GetArea()
.     {
.         return _size * _size;
.     }
. }
> 
> Square square = new Square(10);
> square.GetArea()
100

43.10 자식 클래스에만 멤버 상속하기

protected는 자식 클래스들까지만 접근 가능한 멤버

43.11 기본 클래스의 멤버 숨기기

부모 클래스에 만든 특정 메서드를 자식 클래스에서 새롭게 정의해서 사용할 때 new 키워드를 사용하면 된다.
이를 메서드 오버라이드라고도 한다.

> class Parent
. {
.     public void Work() => Console.WriteLine("programmer");
. }
> 
> class Child : Parent
. {
.     public new void Work() => Console.WriteLine("progamer"); 
. }
> 
> var child = new Child();
> child.Work();
progamer

new를 생략할수도 있지만, 명시적으로 사용해야 좋음. 컴파일러도 경고 안 날리고.

gpt는 new는 숨김이고, 메서드 오버라이딩이 아니라고 함.

특징숨김(shadowing)오버라이딩(overriding)
키워드newoverride
부모 멤버 요구사항특별한 요구사항 없음부모의 멤버는 virtual 또는 abstract이어야 함
호출 결정 시점컴파일 타임 (정적 바인딩)런타임 (동적 바인딩)
호출 방식변수 타입에 따라 결정실제 객체 타입에 따라 결정
다형성 지원다형성 불가능다형성 가능
부모 멤버 상태여전히 호출 가능 (부모 참조 사용 시)부모 멤버는 자식 메서드에 의해 재정의됨

44 메서드 오버라이드

44.1 메서드 오버라이드: 재정의

메서드 오버라이드 : 부모 클래스에 만들었던 메서드를 동일한 이름으로 자식 클래스에서 다시 정의해서 사용하는 것
부모 클래스에 virtual 키워드로 선언해놓은 메서드는 자식 클래스에서 override 키워드로 재정의해서 사용 가능하다.

44.2 상속 관계에서 메서드 오버라이드

> public class Parent
. {
.     public void Say() => Console.WriteLine("parent_hi");
.     public void Run() => Console.WriteLine("parent_run");
.     public virtual void Walk() => Console.WriteLine("parent_walk");
. }
> 
> public class Child : Parent
. {
.     public void Say() => Console.WriteLine("child_hi");
.     public new void Run() => Console.WriteLine("child_run");
.     public override void Walk() => Console.WriteLine("child_walk");
. }
> Child c = new Child();
> c.Say();
child_hi
> c.Run();
child_run
> c.Walk();
child_walk

메서드 재정의하는 세 가지 방법

44.3 메서드 오버로드와 오버라이드

오버로드(overload)는 여러 번 정의하는 것
오버라이드는 다시 정의하는 것

> static void Print(int number) => Console.WriteLine(number);
> static void Print(ref int number) => Console.WriteLine(++number);
> var number = 100;
> Print(number);
100
> Print(ref number);
101
> Print(number);
101

매개변수에 따라 호출되는 함수가 다르다. 이게 오버로드

44.4 메서드 오버라이드 봉인

메서드에도 sealed 키워드를 붙여 더 이상 오버라이드해서 사용하지 못하도록 설정 가능

45 인터페이스

45.1 인터페이스

인터페이스를 사용하면 전체 프로그램의 설계도에 대한 명세서를 작성할 수 있다.
인터페이스는 클래스 또는 구조체에 포함될 수 있는 관련 있는 메서드들을 묶어 관리한다.
인터페이스는 명세서(specification)(규약, 표준) 역할을 한다.
인터페이스를 상속받아 그 내용을 구현하는 클래스는 인터페이스에 선언된 멤버가 반드시 구현되어 있다고 보장한다.

  • 인터페이스는 interface 키워드로 만든다. 인터페이스엔 실행 가능한 코드와 데이터가 없다. (원래는 없는데 나중에 추가됨)
  • 인터페이스 내의 모든 멤버는 기본적으로 public이다.
  • C#에서 인터페이스 이름은 ICar, IFood 등 대문자 I로 시작한다.
namespace InterfaceNote
{
    interface ICar
    {
        void Go();
    }

    class Car : ICar
    {
        public void Go() => Console.WriteLine("상속한 인터페이스에 정의된 모든 멤버를 반드시 구현해야 함");
    }

    class InterfaceNote
    {
        static void Main()
        {
            var car = new Car();
            car.Go();
        }
    }
}

45.2 인터페이스 형식 개체에 인스턴스 담기

IRepository repository = new Repository();

45.3 생성자의 매개변수에 인터페이스 사용하기

생성자의 매개변수에 인터페이스 형식을 사용하면 해당 인터페이스를 상속하는 모든 클래스의 인스턴스를 받을 수 있다.

namespace InterfaceDemo
{
    interface IBattery
    {
        string GetName();
    }

    class Good : IBattery
    {
        public string GetName() => "Good";
    }

    class Bad : IBattery
    {
        public string GetName() => "Bad";
    }

    class Car
    {
        private IBattery _battery;

        public Car(IBattery battery)
        {
            _battery = battery;
        }

        public void Run() => Console.WriteLine("{0} 배터리를 장착한 자동차가 달립니다." , _battery.GetName());
    }

    class InterfaceDemo
    {
        static void Main(string[] args)
        {
            var good = new Car(new Good()); good.Run();
            new Car(new Bad()).Run();
        }
    }
}

45.4 인터페이스를 사용한 다중 상속 구현하기

C#에서 클래스는 단일 상속만 지원하고,
인터페이스는 여러 개 상속받을 수 있다.

class Dog : IAnimal, IDog

45.5 명시적인 인터페이스 구현하기

인터페이스는 다중 상속이 가능하기 때문에, 각 인터페이스에 동일한 멤버가 구현되어 있을 때가 있다.
이때는 명시적으로 어떤 인터페이스의 멤버를 실행할지 지정해야 한다.

어차피 인터페이스에는 구현 없고 상속받는 클래스가 알아서 하는거니까 지정 안해도 상관 없지 않아?

1. 명시적 인터페이스 구현이 필요한 이유

명시적 인터페이스 구현은 다음과 같은 상황에서 유용합니다:

(1) 다중 상속에서 구현이 달라야 할 때

만약 IA.DoWork()IB.DoWork()의 동작이 달라야 한다면, 명시적 인터페이스 구현을 사용해야 합니다.

interface IA
{
    void DoWork();
}

interface IB
{
    void DoWork();
}

class MyClass : IA, IB
{
    void IA.DoWork()
    {
        Console.WriteLine("IA의 DoWork 실행");
    }

    void IB.DoWork()
    {
        Console.WriteLine("IB의 DoWork 실행");
    }
}

호출 방식:

IA a = new MyClass();
a.DoWork(); // IA의 DoWork 실행

IB b = new MyClass();
b.DoWork(); // IB의 DoWork 실행

위 코드에서 MyClassIAIBDoWork()를 각각 다르게 구현하여 두 인터페이스를 구분할 수 있습니다.


(2) 인터페이스 멤버를 숨기고 싶을 때

명시적 인터페이스 구현은 인터페이스의 멤버를 클래스의 공용 API로 노출하지 않고 숨길 수 있는 방법입니다.

interface IPrivate
{
    void Secret();
}

class MyClass : IPrivate
{
    void IPrivate.Secret()
    {
        Console.WriteLine("비밀 작업 수행 중...");
    }
}

호출 방식:

var myClass = new MyClass();
// myClass.Secret(); // 오류: IPrivate.Secret은 직접 호출할 수 없음

IPrivate privateAccess = myClass;
privateAccess.Secret(); // 비밀 작업 수행 중...
  • IPrivate.Secret()은 인터페이스를 통해서만 호출할 수 있습니다.
  • 클래스의 외부 사용자가 불필요한 인터페이스 멤버에 접근하지 못하게 제어할 수 있습니다.

2. 명시적 구현이 불필요한 경우

명시적 구현이 필요하지 않은 경우는 다음과 같습니다:

  • 여러 인터페이스에 동일한 멤버 이름과 시그니처가 있지만, 동작이 동일한 경우:
  interface IA
  {
      void DoWork();
  }

  interface IB
  {
      void DoWork();
  }

  class MyClass : IA, IB
  {
      public void DoWork()
      {
          Console.WriteLine("동일한 DoWork 실행");
      }
  }
  • MyClass.DoWork()IAIB 모두의 요구사항을 만족하므로 별도로 구분할 필요가 없습니다.

Cast<T>() 메서드로 List<자식>을 List<부모>로 변환

interface A {}
class B : A {}

List<A> convertAll = (new List<B>).ConvertAll(x => (A)x);
List<A> astoff = (new List<B>()).Cast<A>().ToList();

자식 클래스의 컬렉션 인스턴스를 부모 클래스의 컬렉션 인스턴스에 대입할 땐 ConvertAll() 또는 Cast<T>() 메서드를 사용할 수 있다.

45.7 IEnumerator 인터페이스 사용하기

IEnumerator 인터페이스는 문자열 배열 등 GetEnumerator() 메서드의 결괏값을 담아 MoveNext() 메서드로 값이 있는지 확인하고, Current 속성으로 현재 반복되는 데이터를 가져다 사용할 수 있다.

45.8 IDisposable 인터페이스 사용하기

IDisposable을 상속하는 클래스는 Dispose() 메서드를 구현해야 한다.
이 메서드는 해당 클래스의 개체를 다 사용한 후 마지막으로 호출해서 정리하는 역할을 한다.



profile
너 정말 **핵심**을 찔렀어

0개의 댓글