num.ToString("N2");
ToString 자체를 새로 배워서 작성하기 보다는 뒤에 괄호 안에 N2를 주목해보자. N2는 쉽게 말하면 형식 문자열이다. 숫자를 다양한 형식으로 출력할 수 있으며 아래와 같은 다양한 숫자 서식이 있다.
| 형식 지정자 | 종류 | 값의 예 | 다양한 출력의 예 |
|---|---|---|---|
| N / n | 숫자 Number | int a = 12345; double b = 2345.6789; | 12345 12,345.00 12,345 2,345.6789 2,345.68 2,346 |
| F / f | 고정 소수점 Fixed-point | int a = 12345; double b = 2345.6789; | 12345 12345.00 12345 2345.6789 2345.68 2346 |
| C / c | 통화 Currency | int a = 1234; | ₩1,234 $1,234 |
| D / d | 10진법 Decimal | int a = 1234; | 1234001234 |
| E / e | 지수 | double a = 123.45; | 1.234500E+002 |
| G / g | 일반 General | F 또는 E 형식 중에서 간단한 형식으로 출력 | |
| P / p | 백분율 Percentage | double a = 0.567; | 56.70% 56.7% |
| X / x | 16진법 Hexadecimal | int a=255; | FF |
따라서 N2의 경우 숫자를 소수 둘째자리까지 표기하겠다는 뜻이다.
ToString과 String.Format으로 소숫 자리수를 표현하는 방식의 차이점은 뭘까?
str = num.ToString("N2");
str = string.Format("{0:f2}",num);
실제로 위 두 개의 코드 자체가 하는 일은 동일하다.
하지만 ToString의 경우 단일 값에만 사용 가능하다. 즉, num이라는 하나의 정수 변수만 문자열로 바꿀 수 있다. 두 개의 정수형 변수를 한 번에 바꿀 수는 없다.
string.Format은 아래와 같은 사용이 가능하다.
int hours = 5;
int minutes = 9;
string result = string.Format("{0:00}:{1:00}", hours, minutes); // 결과: "05:09"
이처럼 ToString은 하나의 값에만 적용되는 반면 string.Format은 여러 값을 포맷팅하여 하나의 문자열로 결합할 수 있다.
따라서 특정 변수 하나만 문자열로 포맷팅 후에 사용하고 싶으면 ToString이, 여러 값을 복합적으로 포맷하고, 한 번에 다양한 형식을 적용할 필요가 있으면 string.Format이 더 좋을 것 같다.
예를 들어 점수나 시간 같은 값을 문자열로만 바꾸기 위해서는
ToString이, 어떠한 NPC와 플레이어 사이 간의 대화에 플레이어의 네임이나, 특정 값이 포함된 대화문을 만들 때는string.Format이 편리할 것 같다.
성능 차이는 없을까? 리소스나, 메모리 사용 측면으로? 나중에 시간이 되면 포맷팅이 어떤 과정으로 이뤄지는지도 참고 삼아 보면 좋을 것 같다.
진행하면서 애니메이션을 실행할 때 Time.timeScale = 0.0f 때문에 애니메이션이 모두 실행되기도 전에 시간이 멈춰서 애니메이션이 정상적으로 실행되지 않는 문제를 Invoke문을 사용해서 수정해주었다.
이 timeScale과 Animation에 관한 내용도 전에 다뤘던 적이 있는것 같아 찾아보았다.
[Unity][3D-Game] Tower Defense Game (19)

위와 같이 컴포넌트를 Unscaled Time으로 해줘도 동일하게 timeScale이 0이어도 애니메이션 실행이 된다.
별도의 특정 작업을 지연 실행하는 목적이 아니라 단순히 시간의 영향을 받지 않고 애니메이션을 재생해야 한다면 TimeStop 함수를 따로 만들고 Invoke로 실행하는 것보다는 Unscaled Time이 더 효율적이긴 하다.
Invoke는 단순히 지연 실행에 있어서Coroutine보단 간편할 지는 모르겠지만 매개변수 전달이 안 된다는 단점이 있다. 더 많은 차이점이 있지만 다 적는것보다는 정리해 놓은 좋은 문서가 있어서 해당 문서를 참고하면 좋을 것 같다.
void Update() {
if (isPlay) {
time += Time.deltaTime;
timeText.text = time.ToString("N2");
}
}
왜 isPlay를 써야할까? 대강이나마 충돌이 발생한 순간 GameOver가 실행되고 해당 시간을 그대로 사용하기 위해 시간의 변경을 막고자 isPlay를 사용한다고는 알고 있는데 자세히 알고 있는게 도움이 될 것 같아 추가적으로 조사해보았다.
자세한 절차는 아래와 같이 이뤄진다.
충돌 감지 메서드에서 GameOver가 호출된 후, 현재 프레임 내에서 Update 메서드가 여전히 호출되고 있을 수 있다. 이 상황에서 time 값이 갱신되고 있으면, 실제 GameOver 메서드에서 출력된 time 값이 실제 time 값과 다를 수 있다.
즉, GameOver 호출되고 난 이후에 Update로 인해 time 값의 갱신이 일어날 수 있다.
저번 TIL (1)에서 다뤘던 사진인데 OnCollision는 Update보다 먼저 처리된다.
이는 GameOver이 호출되고 난 이후에 Update문이 실행됨을 뜻한다. > GameOver 후에 time 값이 갱신됨
그렇기 때문에 EndPanel의 시간(nowScore)과 실제 상단의 시간이 다를 수 있음을 방지하기 위해 isPlay를 사용한다.
Panel이 점수를 가리게 하면 굳이 isPlay를 안 써도 되지 않을까?
public void GameOver() {
isPlay = false;
anim.SetBool("isDie", true);
Invoke("TimeStop", 0.5f);
nowScore.text = time.ToString("N2");
string key = "bestScore";
if (PlayerPrefs.HasKey(key)) {
float best = PlayerPrefs.GetFloat(key);
if (best < time) {
PlayerPrefs.SetFloat(key, time);
bestScore.text = time.ToString("N2");
}
else {
bestScore.text = best.ToString("N2");
}
}
else {
PlayerPrefs.SetFloat(key, time);
bestScore.text = time.ToString("N2");
}
endPanel.SetActive(true);
}
실제로 이 코드 보다는 아래와 같은 코드로 정리해서 더 가독성을 향상시킬 수 있다.
public void GameOver() {
isPlay = false;
anim.SetBool("isDie", true);
Invoke("TimeStop", 0.5f);
nowScore.text = time.ToString("N2");
string key = "bestScore";
float best = PlayerPrefs.GetFloat(key, 0f);
if (time > best) {
PlayerPrefs.SetFloat(key, time);
best = time;
}
bestScore.text = best.ToString("N2");
endPanel.SetActive(true);
}
PlayerPrefs.GetFloat(key, 0f)를 사용해서 키가 없을 경우 기본 값을 0f로 가져오게 해서 HasKey를 굳이 확인하지 않고 진행되도록 수정해주었다.
for (int i = 1; i <= 100; i++)
if (i % 2 == 1) Console.WriteLine(i);
int j = 1;
while (j <= 100)
{
if (j % 2 == 1) Console.WriteLine(j);
j += 1;
}
int k = 1;
do
{
if (k % 2 == 1) Console.WriteLine(k);
k += 1;
} while (k <= 100);
int[] numbers = { 10, 20, 30, 40, 50 };
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
{
sum += numbers[i];
}
float average = sum / numbers.Length;
Console.WriteLine($"Sum: {sum}");
Console.WriteLine($"Average: {average}");
Console.Write("Enter a number: ");
int number = int.Parse(Console.ReadLine());
int fact = 1;
for (int i = 1; i <= number; i++)
{
fact *= i;
}
Console.WriteLine($"Factorial of {number} is {fact}");
Random random = new Random();
int targetNumber = random.Next(1, 101);
int guess = 0;
do
{
Console.Write("Enter your guess (1-100): ");
guess = int.Parse(Console.ReadLine());
if (targetNumber == guess)
{
Console.WriteLine("Congratulations! You guessed the number.");
break;
}
else if (targetNumber > guess)
{
Console.WriteLine("Too low! Try again.");
}
else
{
Console.WriteLine("Too high! Try again.");
}
} while (true);
for (int i = 1; i <= 9; i++)
{
for (int j = 2; j <= 9; j++ )
{
Console.Write("{0} * {1} = {2}\t", j, i, j*i);
}
Console.WriteLine();
}
int[] numbers = { 10, 20, 30, 40, 50 };
int max = numbers[0];
int min = numbers[0];
for (int i = 0; i < numbers.Length; i++)
{
if (numbers[i] > max) max = numbers[i];
if (numbers[i] < min) min = numbers[i];
}
Console.WriteLine($"Max: {max}");
Console.WriteLine($"Min: {min}");