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을 통해 키 입력하면 바로 할당 가능)
Player Input이 활성화 될 때 키가 중립으로 되어있는지 확인
입력을 편집해주는 기능 (출력값의 크기를 바꾸거나 반대로 반전)
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"
/// <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가지
esc 누르면 pause, esc 한 번 더 누르거나 resume 버튼 누르면 resume을 연결한 이후 버그 발생
디버그
구현 완료
클래스 간에 부모와 자식 관계를 설정하는 것을 개체 관계 프로그래밍이라 한다.
상속은 부모 클래스에 정의된 내용을 다시 사용하거나 확장 또는 수정하여 자식 클래스로 만드는 것.
상속 : 부모 클래스의 모든 멤버를 자식 클래스가 재사용하도록 허가하는 기능
C#은 단일 상속만 지원함. 다중 상속은 인터페이스로만 할 수 있음.
public class 기본클래스이름
{
// 기본 클래스의 멤버 정의
}
public class 파생클래스이름 : 기본클래스이름
{
// 기본 클래스의 멤버를 포함한 자식 클래스의 멤버 정의
}
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();
}
}
}
> 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
클래스 내에서
this는 자신을 의미하고, this()는 자신의 생성자를 나타낸다.
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()에 있는 괄호는 없어도 됨
봉인(sealed) 클래스 : 더 이상 다른 클래스에 상속되지 않게 사용하는 클래스. 최종 클래스라고도 하며, 선언부에 sealed 키워드를 붙여 만든다.
sealed partial class App : Application
클래스를 선언할 때 추가로 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
protected는 자식 클래스들까지만 접근 가능한 멤버
부모 클래스에 만든 특정 메서드를 자식 클래스에서 새롭게 정의해서 사용할 때 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) |
|---|---|---|
| 키워드 | new | override |
| 부모 멤버 요구사항 | 특별한 요구사항 없음 | 부모의 멤버는 virtual 또는 abstract이어야 함 |
| 호출 결정 시점 | 컴파일 타임 (정적 바인딩) | 런타임 (동적 바인딩) |
| 호출 방식 | 변수 타입에 따라 결정 | 실제 객체 타입에 따라 결정 |
| 다형성 지원 | 다형성 불가능 | 다형성 가능 |
| 부모 멤버 상태 | 여전히 호출 가능 (부모 참조 사용 시) | 부모 멤버는 자식 메서드에 의해 재정의됨 |
메서드 오버라이드 : 부모 클래스에 만들었던 메서드를 동일한 이름으로 자식 클래스에서 다시 정의해서 사용하는 것
부모 클래스에 virtual 키워드로 선언해놓은 메서드는 자식 클래스에서 override 키워드로 재정의해서 사용 가능하다.
> 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
메서드 재정의하는 세 가지 방법
오버로드(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
매개변수에 따라 호출되는 함수가 다르다. 이게 오버로드
메서드에도 sealed 키워드를 붙여 더 이상 오버라이드해서 사용하지 못하도록 설정 가능
인터페이스를 사용하면 전체 프로그램의 설계도에 대한 명세서를 작성할 수 있다.
인터페이스는 클래스 또는 구조체에 포함될 수 있는 관련 있는 메서드들을 묶어 관리한다.
인터페이스는 명세서(specification)(규약, 표준) 역할을 한다.
인터페이스를 상속받아 그 내용을 구현하는 클래스는 인터페이스에 선언된 멤버가 반드시 구현되어 있다고 보장한다.
interface 키워드로 만든다. 인터페이스엔 실행 가능한 코드와 데이터가 없다. (원래는 없는데 나중에 추가됨)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();
}
}
}
IRepository repository = new Repository();
생성자의 매개변수에 인터페이스 형식을 사용하면 해당 인터페이스를 상속하는 모든 클래스의 인스턴스를 받을 수 있다.
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();
}
}
}
C#에서 클래스는 단일 상속만 지원하고,
인터페이스는 여러 개 상속받을 수 있다.
class Dog : IAnimal, IDog
인터페이스는 다중 상속이 가능하기 때문에, 각 인터페이스에 동일한 멤버가 구현되어 있을 때가 있다.
이때는 명시적으로 어떤 인터페이스의 멤버를 실행할지 지정해야 한다.
어차피 인터페이스에는 구현 없고 상속받는 클래스가 알아서 하는거니까 지정 안해도 상관 없지 않아?
명시적 인터페이스 구현은 다음과 같은 상황에서 유용합니다:
만약 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 실행
위 코드에서 MyClass는 IA와 IB의 DoWork()를 각각 다르게 구현하여 두 인터페이스를 구분할 수 있습니다.
명시적 인터페이스 구현은 인터페이스의 멤버를 클래스의 공용 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()은 인터페이스를 통해서만 호출할 수 있습니다.명시적 구현이 필요하지 않은 경우는 다음과 같습니다:
interface IA
{
void DoWork();
}
interface IB
{
void DoWork();
}
class MyClass : IA, IB
{
public void DoWork()
{
Console.WriteLine("동일한 DoWork 실행");
}
}
MyClass.DoWork()는 IA와 IB 모두의 요구사항을 만족하므로 별도로 구분할 필요가 없습니다.
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>()메서드를 사용할 수 있다.
IEnumerator 인터페이스는 문자열 배열 등 GetEnumerator() 메서드의 결괏값을 담아 MoveNext() 메서드로 값이 있는지 확인하고, Current 속성으로 현재 반복되는 데이터를 가져다 사용할 수 있다.
IDisposable을 상속하는 클래스는 Dispose() 메서드를 구현해야 한다.
이 메서드는 해당 클래스의 개체를 다 사용한 후 마지막으로 호출해서 정리하는 역할을 한다.