기초
글 64개
원점에서 멀어지면 떨리는 이유
오픈월드에서 원점에서 수십 km 떨어지면 캐릭터와 카메라가 덜덜 떨립니다. float 는 큰 수일수록 표현할 수 있는 값 사이가 벌어져서, 작은 움직임이 반올림으로 사라지거나 부풀기 때문입니다. 세상을 원점 쪽으로 다시 옮겨서 막습니다.
빠른 총알이 벽을 뚫고 지나가는 이유
물리는 0.02초마다 그 순간의 자리만 보고 겹쳤는지 검사합니다. 한 걸음에 가는 거리가 벽 두께보다 크면, 벽 앞과 벽 뒤만 보고 부딪힌 적이 없다고 판단합니다. 연속 충돌 검사와 레이캐스트로 그 사이를 메웁니다.
리지드바디가 떨려 보이는 이유
물리는 1초에 50번 움직이고 화면은 그보다 자주 그리니, Rigidbody 는 몇 프레임 제자리에 있다가 한 번에 뜁니다. deltaTime 으로는 안 고쳐지고, Interpolate 가 직전 두 물리 위치 사이를 메워 그려서 고칩니다.
FixedUpdate 가 따로 도는 이유
유니티 물리는 화면 프레임과 상관없이 0.02초마다 한 번씩 계산합니다. 프레임마다 지난 시간을 모아 두었다가 0.02초씩 덜어 내며 FixedUpdate 를 부르기 때문에, 한 프레임에 0번 불리기도 하고 두 번 불리기도 합니다. 간격을 고정하는 이유와 그 때문에 생기는 입력 씹힘을 숫자로 따라갑니다.
점프가 포물선이 되는 이유
중력은 위치가 아니라 속도를 바꿉니다. 매 프레임 속도에 중력을, 위치에 속도를 더하면 포물선이 나옵니다. 속도 · 가속도 · 포물선을 작은 숫자로 처음부터 따라가고, 계산 간격이 크면 점프 높이가 달라지는 이유와 원하는 높이로 뛰게 하는 처음 속도 √(2gh) 까지 봅니다.
매 프레임 Lerp(a, b, 0.1f) 를 쓰면 생기는 일
카메라를 부드럽게 따라가게 하려고 매 프레임 Lerp 에 0.1 을 넣으면, fps 가 높은 기계에서 더 빨리 따라갑니다. deltaTime 을 곱해도 딱 맞지 않습니다. 남은 거리가 곱셈으로 줄어들기 때문이고, 비율을 1 − Exp(−k × deltaTime) 로 주면 fps 와 상관없어집니다. 거듭제곱부터 숫자로 따라갑니다.
Time.deltaTime 을 곱하는 이유
내 컴퓨터에서는 알맞던 캐릭터가 다른 컴퓨터에서는 두 배로 빨라집니다. Update 가 1초에 몇 번 불릴지 모르기 때문이고, deltaTime 은 "한 번에 얼마" 를 "1초에 얼마" 로 바꿔 줍니다.
쿼터니언을 몰라도 쓰는 법
쿼터니언은 "축 하나와 그 축으로 돈 각도" 를 네 숫자에 담은 회전입니다. 네 숫자를 직접 건드리지 않고, 함수로 만들고, 곱해서 합치고, q * v 로 벡터를 돌립니다. 곱하는 순서가 월드 기준과 자기 기준을 가릅니다.
오일러 각: eulerAngles 가 넣은 값과 다르게 나오고 짐벌 락이 생기는 이유
x 를 100 으로 넣은 회전을 읽으면 (80, 180, 180) 이 나옵니다. 오일러 각은 세 축으로 차례로 세 번 도는 것이라 같은 자세를 여러 숫자로 쓸 수 있고, x 가 90 이 되면 두 축이 같은 일을 해서 회전 방향 하나를 잃는 짐벌 락이 생깁니다. 축과 회전부터 따라갑니다.
로컬 좌표와 월드 좌표
같은 (2, 1) 도 월드 기준이면 한 자리, 캐릭터 기준이면 다른 자리입니다. 좌표계와 기저가 무엇인지부터, TransformPoint 가 축을 섞어 월드 자리로 옮기고 InverseTransformPoint 가 내적으로 되돌리는 과정을 작은 숫자로 따라갑니다.
행렬: 크기 · 회전 · 이동을 한 번에 담는 표
부모를 돌리면 자식도 같이 도는 것은 엔진이 꼭짓점마다 행렬을 곱하기 때문입니다. 행렬이 무엇인지부터, 행렬 × 벡터가 열을 섞는 계산이라는 것, 크기 · 회전 · 이동을 TRS 순서로 합치는 이유까지 작은 숫자로 따라갑니다.
투영: 벽에 막힌 이동을 벽을 따라 미끄러지게 바꾸기
벽에 비스듬히 부딪힌 캐릭터가 멈추지 않고 벽을 따라 미끄러지게 하려면, 이동 벡터에서 벽으로 파고드는 조각만 빼면 됩니다. 그 조각을 내적으로 구하는 법과 ProjectOnPlane · Reflect 가 하는 일을 작은 숫자로 따라갑니다.
외적: 왼쪽인가 오른쪽인가
내적은 앞뒤만 알려 주고 왼쪽과 오른쪽은 가리지 못합니다. 외적은 두 벡터에 모두 수직인 벡터를 만들고, 바닥 위의 두 방향이면 그 y 부호가 왼쪽과 오른쪽을 알려 줍니다. 수직 · 법선 · 왼손 좌표계부터 작은 숫자로 따라갑니다.
내적: 적이 앞에 있나 뒤에 있나
백어택 판정과 시야각 판정은 Vector3.Dot 한 줄입니다. 내적은 같은 자리끼리 곱해서 더한 숫자이고, 부호는 앞과 뒤를, 값은 사이각의 cos 를 알려 줍니다.
방향 (x, y) 로 각도 구하기: 총구를 마우스로 돌리는 Atan2
2D 게임에서 총구를 마우스 쪽으로 돌리려면 방향 (x, y) 에서 각도를 구해야 합니다. 세로 ÷ 가로 가 tan 이고, 그 비율에서 각도를 거꾸로 찾는 것이 Atan 입니다. Atan 이 정반대 방향을 가리지 못하는 이유와 Atan2 가 그것을 고치는 과정을 숫자로 따라갑니다.
sin 과 cos 는 무엇인가: 비스듬한 화살표를 가로와 세로로 나누기
대포를 30도로 쏠 때 앞으로 얼마, 위로 얼마 가는지 구하는 도구가 cos 와 sin 입니다. 직각삼각형에서 가로 ÷ 빗변 이 cos, 세로 ÷ 빗변 이 sin 이고, 길이 1 인 원 위의 점이 (cos, sin) 입니다. 유니티의 Mathf.Sin 이 도가 아닌 라디안을 받는 이유까지 수학을 처음부터 따라갑니다.
대각선으로 가면 빨라지는 이유
W 와 D 를 같이 누르면 캐릭터가 41% 빨라집니다. 입력 (1, 1) 의 길이가 1 이 아니라 √2 라서입니다. 벡터의 길이를 재는 법과, 길이를 1 로 맞추는 정규화를 언제 쓰고 언제 쓰지 않는지 봅니다.
벡터 더하기는 화살표를 이어 붙이는 것이다
걷기, 바람, 넉백처럼 여러 움직임을 한 번에 합치는 방법은 벡터 더하기입니다. 같은 자리끼리 더하면 화살표가 이어 붙고, 숫자를 곱하면 방향은 두고 길이만 바뀝니다.
벡터는 위치가 아니라 화살표다
Vector3 하나에 위치도 방향도 담기지만 둘은 다른 것입니다. 벡터는 시작점 없이 방향과 길이만 가진 화살표이고, 두 위치 사이의 방향은 도착 − 출발로 구합니다.
값이 없다는 것도 상태다
null 은 0이 아니라 가리키는 대상이 없는 것입니다. 없을 수 있다는 사실을 타입에 적어 두면 컴파일러가 봐 줍니다.
예외는 정상 흐름과 오류 흐름을 갈라 놓는다
매 줄마다 성공했는지 확인하지 않아도 됩니다. 정상 코드는 정상 코드대로 두고 오류 처리는 따로 모읍니다.
이벤트는 일이 생기면 알려 달라는 약속이다
알리는 쪽은 누가 듣는지 모르고, 듣는 쪽은 언제 일어날지 모릅니다. 서로를 몰라도 됩니다.
람다는 이름 없이 그 자리에서 만드는 메서드다
한 번 쓰고 말 동작에 이름을 붙이지 않습니다. 넘기는 자리에 바로 적습니다.
대리자는 메서드를 담는 변수다
값이 아니라 무엇을 할지를 담습니다. 부르는 쪽과 실제 동작을 떼어 놓을 수 있습니다.
프로퍼티는 필드 앞에 세운 창구다
겉보기엔 필드처럼 쓰지만 읽고 쓸 때 코드가 실행됩니다. 캡슐화를 문법으로 만든 것입니다.
foreach 는 번호를 신경 쓰지 않는다
처음부터 끝까지 하나씩 꺼내 줍니다. 인덱스가 필요 없는 반복에서 실수할 자리가 사라집니다.
T 는 여기에 타입이 들어온다는 빈칸이다
object 로 다 받으면 타입 검사가 사라지고 박싱이 일어납니다. 제네릭은 타입을 나중에 정하면서 둘 다 막습니다.
무엇인가면 추상 클래스, 무엇을 할 수 있으면 인터페이스
하나는 공통 코드를 물려주면서 빈칸을 남기고, 하나는 지켜야 할 목록만 적습니다. 문장으로 읽어 보면 답이 나옵니다.
같은 호출인데 객체마다 다르게 동작한다
부모 타입으로 불러도 그 객체가 갈아 끼운 메서드가 실행됩니다. 새 자식을 추가해도 부르는 쪽은 안 고칩니다.
상속은 물려받아 덧붙이는 것이다
겹치는 부분을 부모에 두고 다른 것만 자식에 적습니다. 자식을 부모로 보는 것은 언제나 안전합니다.
안은 감추고 창구만 열어 둔다
필드를 열어 두면 잘못된 값이 들어갈 길도 같이 열립니다. 창구를 좁히면 확인할 기회가 생깁니다.
대입하면 값이 복사되거나 주소가 복사된다
규칙은 하나입니다. 담긴 것이 복사됩니다. 담긴 것이 값이냐 주소냐에 따라 결과가 완전히 달라집니다.
생성자는 태어날 때 딱 한 번 실행된다
만들고 나서 값을 채우면 빠뜨릴 수 있습니다. 만드는 순간 채우게 하면 빠뜨릴 방법이 없습니다.
new 는 힙에 자리를 만들고 주소를 돌려준다
변수 안에 객체가 들어 있는 것이 아닙니다. 객체가 있는 곳의 주소가 들어 있습니다.
클래스는 틀이고 객체는 찍어낸 것이다
관련 있는 데이터와 그 데이터를 다루는 코드를 한 덩어리로 묶습니다. 틀은 하나지만 찍어낸 것은 여러 개입니다.
부른 순서대로 쌓이고 반대로 풀린다
메서드가 자기를 부르면 호출이 겹겹이 쌓입니다. 쌓이는 자리에는 한계가 있고, 짧은 코드가 빠른 코드는 아닙니다.
이름은 같고 투입구만 다르다
Console.WriteLine 에 숫자를 넣어도 문자열을 넣어도 되는 이유입니다. 같은 이름의 메서드가 여러 벌 있습니다.
return 은 값을 돌려주면서 메서드를 끝낸다
두 가지 일을 동시에 합니다. 끝낸다는 쪽을 이용하면 깊게 겹친 if 를 평평하게 펼 수 있습니다.
ref 는 고치러, out 은 채우러 간다
하나는 이미 있는 값을 바꾸러 가고, 하나는 없던 값을 만들어 옵니다. TryParse 가 두 번째 방식입니다.
넘어가는 것은 값의 사본이다
메서드 안에서 매개변수를 바꿔도 밖의 원본은 그대로입니다. 값을 적어 보낸 것이지 변수를 보낸 것이 아닙니다.
메서드 한 줄은 네 부분으로 되어 있다
무엇을 돌려주고, 이름이 무엇이고, 무엇을 받고, 무슨 일을 하는가. 이 넷만 정하면 메서드가 됩니다.
"123" 은 숫자 123 이 아니다
입력으로 받은 것은 언제나 문자열입니다. Parse 는 바꿀 수 없으면 프로그램을 멈추고, TryParse 는 못 바꿨다고 알려주기만 합니다.
같은지 보는 것과 줄 세우는 것은 다른 일이다
== 는 문자열의 내용을 비교합니다. 대소문자를 무시하려면 ToLower 로 맞추는 대신 비교 방식을 넘겨야 하고, 정렬 순서가 필요하면 Compare 를 씁니다.
자르고 찾는 것은 전부 새 문자열이다
Substring 의 둘째 인자는 끝 번호가 아니라 개수입니다. IndexOf 는 못 찾으면 -1 을 돌려줍니다. 두 가지만 알아도 문자열 버그의 대부분이 없어집니다.
적은 그대로가 값이 되는 문자열
따옴표는 문자열의 끝을 알리는 기호라서 안에 그대로 쓸 수 없습니다. 역슬래시로 피하는 방법과, 그마저 필요 없게 만드는 따옴표 세 개를 봅니다.
값은 그대로 두고 모양만 바꾼다
$ 는 문자열 안에 값을 끼워 넣고, 콜론 뒤에 적는 형식은 그 값을 어떻게 보여줄지만 정합니다. 캐스팅과 달리 값 자체는 건드리지 않습니다.
바깥은 행, 안쪽은 열이다
바깥이 한 번 돌 때마다 안쪽은 처음부터 끝까지 전부 돕니다. 별찍기는 줄마다 개수를 세는 문제입니다.
break 는 나가고, continue 는 건너뛴다
하나는 반복문을 끝내고 하나는 이번 회차만 포기합니다. continue 는 반복을 멈추지 않습니다.
for 는 적힌 순서대로 실행되지 않는다
괄호에 세 가지가 나란히 적혀 있지만 셋째는 본문이 끝난 뒤에 실행됩니다. 그래서 본문에서 보는 i 는 아직 증가 전입니다.
do-while 은 일단 한 번은 한다
while 은 조건을 먼저 보고 do-while 은 본문을 먼저 실행합니다. 물어보기 전에 한 번은 해야 하는 일에 씁니다.
모든 반복문은 세 부분으로 되어 있다
어디서 시작하고, 언제까지 돌고, 매번 무엇이 변하는가. 셋 중 하나가 빠지면 영원히 돌거나 한 번도 안 돕니다.
코드에 적히지 않은 약속은 없는 약속이다
case 4 가 목요일이라는 사실이 코드 어디에도 없습니다. const 와 enum 은 그 약속을 코드 안으로 옮기는 문법입니다.
switch 는 조건이 아니라 값을 본다
else if 사슬이 같은 변수를 계속 다시 묻고 있다면 switch 자리입니다. 범위는 if, 목록은 switch입니다.
조건을 쓰는 순서가 곧 우선순위다
else if 는 위에서부터 처음 맞는 하나만 고르고 나머지는 검사조차 하지 않습니다. 그래서 쓰는 순서가 결과를 바꿉니다.
갈림길을 정하는 것은 bool 하나다
괄호 안이 아무리 길어도 결과는 true 아니면 false 하나입니다. 그 한 값이 실행이 지나갈 길을 고릅니다.
문자열은 바꿀 수 없다
문자열은 문자를 나란히 놓은 것입니다. 인덱스가 0부터인 이유부터, 고칠 수 없어서 + 가 매번 새 문자열을 만드는 것까지 봅니다.
문자는 숫자로 저장된다
'A' 가 왜 하필 65인지, 대문자와 소문자가 왜 32 차이인지. 아스키코드에서 유니코드까지 봅니다.
식은 순서대로 줄어든다
컴파일러는 한 줄을 왼쪽부터 읽지 않습니다. 우선순위가 높은 곳부터 값으로 바꿔 하나만 남을 때까지 줄입니다.
연산자는 값을 만든다
+, %, ==, && 같은 기호들이 각각 무엇을 하고 어떤 값을 만드는지 한 번에 봅니다. = 와 == 의 차이부터 ++ 의 전위와 후위까지.
값을 옮기면 무슨 일이 생기나
넓은 칸으로 갈 때는 채우고, 좁은 칸으로 갈 때는 잘라냅니다. (int)는 반올림이 아니라 버림이고, 10 / 3 은 나눗셈 시점에 이미 3입니다.
0.1 은 2진수로 끝나지 않는다
0.1 + 0.2 가 0.3 이 아닌 이유. 실수는 값을 그대로 담지 않고 부호·지수·가수 세 조각으로 나눠 담습니다.
정수는 직선이 아니라 원이다
int.MaxValue 에 1을 더하면 int.MinValue 가 됩니다. 예외도 경고도 없이 값만 바뀝니다. 왜 하필 최솟값인지 그림으로 봅니다.
음수는 메모리에 어떻게 들어가 있나
음수라고 특별한 표시가 붙지 않습니다. 같은 자리에 비트만 다르게 채워질 뿐입니다. 부호 비트와 2의 보수를 그림으로 봅니다.
변수를 선언하면 무슨 일이 일어나는가
변수는 값을 담는 상자가 아니라 이름이 붙은 메모리 칸입니다. 선언·할당·사용이 각각 언제 무엇을 하는지 그림으로 봅니다.