개발/유니티

[유니티] 유니티 learn #1 다시 기초부터 해보자

kimchangmin02 2025. 8. 23. 11:54

바이브 코딩하다보니깐, 너무 재미가 없다

결과물은 빨리나오기는 하는데, 내가 뭘 하고있늕도 모르겟고..

또한 한번에 너무 많은걸 시키다보니깐, 

화면 전환해서, 상점 표시하는 부분에서 막혓는데, 

뭐가 문제인지 진단하기도 어렵다

기초부터 쌓자

 

 

 

void Update()
    {
        Vector2 position = transform.position;
        position.x = position.x + 0.1f;
        transform.position = position;
    }
// 1. 현재 오브젝트의 위치 값을 복사하여 'position'이라는 Vector2 변수에 저장합니다.
Vector2 position = transform.position;

// 2. 'position' 변수의 x 값을 현재 x 값에 0.1f를 더한 값으로 변경합니다.
position.x = position.x + 0.1f;

// 3. 변경된 'position' 변수의 값을 다시 오브젝트의 실제 위치(transform.position)에 대입합니다.
transform.position = position;

 

 


 

 

1. Private member 'RubyController.movingMagicNumber' is unused (IDE0051)

  • 의미: movingMagicNumber라는 비공개(private) 변수가 선언되었지만 사용되지 않았다는 뜻입니다.
  • 원인: 아래 Update 함수 코드를 보면 movingMagicNumber 변수 대신 숫자 0.01f를 직접 사용하고 있습니다.
  • C#
    void Update()
    {
        Vector2 position = transform.position;
        // movingMagicNumber 변수를 사용하지 않고, 0.01f를 직접 입력했습니다.
        position.x = position.x + 0.01f;
        transform.position = position;
    }

2. Make field readonly (IDE0044)

  • 의미: 이 필드(변수)는 초기화된 후에 값이 전혀 변경되지 않으니, 실수로 다른 곳에서 값을 바꾸지 못하도록 readonly(읽기 전용)로 만드는 것이 좋겠다는 권장 사항입니다.
  • 원인: movingMagicNumber 0.01f로 선언된 이후 코드 어디에서도 값이 바뀌지 않습니다. 이 경고는 첫 번째 '사용되지 않음' 경고 때문에 함께 나타나는 경우가 많습니다.
float movingMagicNumber = 0.01;

 

 

 

 


Time.deltaTime

이전 프레임과 현재 프레임 사이의 시간 간격

 

왜 이게 필요할까요?

Update() 함수는 매 프레임마다 한 번씩 실행됩니다. 그런데 컴퓨터의 성능에 따라 1초에 실행되는 프레임 수(FPS, Frame Per Second)가 다릅니다.

  • 고성능 컴퓨터: 1초에 120 프레임 (120 FPS) 실행
  • 저성능 컴퓨터: 1초에 30 프레임 (30 FPS) 실행

 

 

 

만약 Time.deltaTime 없이 코드를 짜면 어떤 일이 벌어질까요?

position.x = position.x + 0.01f;

  • 고성능 컴퓨터: 1초 동안 Update()가 120번 실행되므로, 1초 후에 x 위치는 0.01 * 120 = 1.2 만큼 이동합니다.
  • 저성능 컴퓨터: 1초 동안 Update()가 30번 실행되므로, 1초 후에 x 위치는 0.01 * 30 = 0.3 만큼 이동합니다.

결과적으로, 고성능 컴퓨터에서 캐릭터가 4배나 더 빨리 움직이는 심각한 문제가 발생합니다!

Time.deltaTime을 사용한 해결책

Time.deltaTime은 이 문제를 완벽하게 해결합니다.

  • 120 FPS 컴퓨터: Time.deltaTime 값은 약 1 / 120 초 입니다.
  • 30 FPS 컴퓨터: Time.deltaTime 값은 약 1 / 30 초 입니다.

이제 이동 코드에 Time.deltaTime을 곱해봅시다. (이동 속도 speed 변수도 추가해 보겠습니다)

position.x = position.x + speed * Time.deltaTime; (여기서 speed가 1이라고 가정)

  • 고성능 컴퓨터 (120 FPS):
    • 한 프레임 이동량: 1 * (1/120)
    • 1초 동안 총 이동량: 120번 * 1 * (1/120) = 1
  • 저성능 컴퓨터 (30 FPS):
    • 한 프레임 이동량: 1 * (1/30)
    • 1초 동안 총 이동량: 30번 * 1 * (1/30) = 1

결론: Time.deltaTime을 곱해주면, 컴퓨터 성능과 관계없이 모든 컴퓨터에서 오브젝트가 동일한 속도로 움직이게 됩니다. 그래서 캐릭터 이동, 총알 발사 등 시간에 따라 변하는 모든 값에는 Time.deltaTime을 곱해주는 것이 거의 필수적입니다. 이를 프레임 독립적인(Frame-rate independent) 처리라고 합니다.

 

 

 


Literal of type double cannot be implicitly converted to type 'float'; use an 'F' suffix to create a literal of this typeCS0664
readonly struct System.Double

float말고 double 쓰자

>ㄴ

 

 

 

 

 

 

Cannot implicitly convert type 'double' to 'float'. An explicit conversion exists (are you missing a cast?)CS0266
double double.operator +(double left, double right)

position의 각 요소(x, y)가 float 타입이기 때문에 발생하는 문제입니다.

결론: 유니티 개발에서는 대부분의 경우 float을 사용하므로, 방법 1과 같이 변수 선언 시 소수 뒤에 f를 붙여 타입을 통일하는 습관을 들이는 것이 가장 좋습니다.

 

 

 


근데 게임화면을 벗어나면, 그만가도록 하고싶은데, 메소드 이름 뭐지 

 

**참고:** 위 코드는 캐릭터의 **중심점(Pivot)** 이 화면 경계를 벗어나지 않도록 합니다. 만약 캐릭터 스프라이트의 **가장자리**가 화면 끝에 정확히 닿게 하려면, 캐릭터 크기의 절반만큼 경계를 안쪽으로 좁혀주어야 합니다. (예: `minX + spriteWidth / 2`)

 

1. float horizontal = Input.GetAxis("Horizontal");

  • 한 줄 요약: 키보드의 좌우 방향키 (A, D 또는 ←, →) 입력을 받는 기능입니다.
  • 자세한 설명:
    • Input: 유니티에서 키보드, 마우스, 조이스틱 등 모든 종류의 입력을 관리하는 클래스입니다.
    • GetAxis(): 축(Axis) 기반의 입력을 받는 메소드입니다. '눌렀다/안눌렀다'의 단순한 on/off가 아니라, 조이스틱처럼 부드러운 값을 받습니다.
    • "Horizontal": 유니티에 미리 설정되어 있는 입력 축의 이름입니다. 이 "Horizontal" 축에는 기본적으로 키보드의 A, D키와 좌우 화살표 키가 연결되어 있습니다.
      • A키 또는 ←키를 누르면 -1에 가까운 값
      • D키 또는 →키를 누르면 +1에 가까운 값
      • 아무것도 안 누르면 0
  • 실제 동작 예시:
    사용자가 D키를 꾸욱 누르면 horizontal이라는 변수에는 부드럽게 0에서 1로 증가하는 값이 저장됩니다. A키를 누르면 -1을 향해 값이 변합니다. 이 값을 이용해 캐릭터를 왼쪽 또는 오른쪽으로 움직일 수 있습니다.

2. Vector2 bottomLeft = cam.ScreenToWorldPoint(...)

  • 한 줄 요약: 화면의 픽셀 좌표(예: 왼쪽 아래 모서리)를 게임 세계의 실제 위치 좌표로 변환해주는 기능입니다.
  • 자세한 설명:
    • 우리 눈에 보이는 게임 화면과 실제 게임 세상은 서로 다른 좌표계를 씁니다.
      • 스크린 좌표 (Screen Space): 픽셀 단위입니다. 왼쪽 아래가 (0, 0), 오른쪽 위가 (화면 너비, 화면 높이) 입니다.
      • 월드 좌표 (World Space): 유니티 에디터에서 보이는 그 좌표입니다. 캐릭터의 transform.position이 바로 이 월드 좌표입니다.
    • cam.ScreenToWorldPoint()는 이 두 좌표계를 이어주는 다리 역할을 합니다. "스크린의 이 픽셀 위치는, 게임 세상에서는 실제 어느 좌표에 해당하나요?" 라는 질문에 답을 해줍니다.
    • new Vector3(0, 0, cam.nearClipPlane): 변환하고 싶은 스크린 좌표를 넣어주는 부분입니다.
      • 0, 0: 스크린의 왼쪽 아래 모서리 픽셀을 의미합니다.
      • cam.nearClipPlane: Z값(깊이)입니다. 2D 좌표를 3D 공간으로 변환하려면 카메라로부터 얼마나 떨어져 있는지를 알려줘야 합니다. 2D 게임에서는 보통 이렇게 카메라가 볼 수 있는 가장 가까운 거리를 기준으로 잡습니다.
  • 실제 동작 예시:
    이 코드를 실행하면 bottomLeft라는 변수에는, 현재 게임 화면의 왼쪽 아래 모서리에 해당하는 실제 게임 월드 좌표 (예: (-8.8, -5.0))가 저장됩니다. topRight에는 오른쪽 위 모서리 좌표가 저장되겠죠. 이제 이 좌표들을 이용해 캐릭터가 나갈 수 없는 벽을 만들 수 있습니다.

3. Vector2 moveDirection = new Vector2(horizontal, vertical);

  • 한 줄 요약: 좌우 입력값과 상하 입력값을 합쳐서 '이동할 방향'을 나타내는 2D 벡터(화살표)로 만드는 과정입니다.
  • 자세한 설명:
    • Vector2는 x와 y, 두 개의 숫자를 담는 주머니입니다. 좌표를 나타낼 수도 있고, 지금처럼 '방향'과 '크기'를 나타낼 수도 있습니다.
    • horizontal: 위에서 설명한 좌우 입력값 (-1 ~ 1)입니다.
    • vertical: Input.GetAxis("Vertical")로 받은 상하 입력값 (W, S키, -1 ~ 1)입니다.
    • new Vector2(horizontal, vertical): 이 두 값을 합쳐 하나의 방향 벡터로 만듭니다.
  • 실제 동작 예시:
    • D키만 누르면: horizontal은 1, vertical은 0이므로 moveDirection (1, 0)이 됩니다. (오른쪽 방향)
    • W키와 A키를 동시에 누르면: horizontal은 -1, vertical은 1이므로 moveDirection (-1, 1)이 됩니다. (왼쪽 위 대각선 방향)
    • 아무것도 안 누르면: (0, 0)이 되어 움직이지 않습니다.

4. newPosition.x = Mathf.Clamp(newPosition.x, minX, maxX);

  • 한 줄 요약: 캐릭터의 x좌표가 정해진 최소값(minX)과 최대값(maxX) 사이를 벗어나지 않도록 강제로 고정시키는 기능입니다.
  • 자세한 설명:
    • Mathf: 유니티에서 제공하는 여러 유용한 수학 함수들이 모여있는 클래스입니다.
    • Clamp(값, 최소값, 최대값): '고정시키다', '틀에 끼우다'라는 뜻입니다. 세 개의 값을 받아서 동작합니다.
      • 만약 '값'이 '최소값'보다 작으면, '최소값'을 돌려줍니다.
      • 만약 '값'이 '최대값'보다 크면, '최대값'을 돌려줍니다.
      • 만약 '값'이 '최소값'과 '최대값' 사이에 있으면, '값'을 그대로 돌려줍니다.
    • minX, maxX: 위에서 ScreenToWorldPoint로 계산해 둔 화면의 왼쪽, 오른쪽 끝 x좌표입니다.
  • 실제 동작 예시:
    화면의 좌우 경계가 minX = -9, maxX = 9라고 가정해 봅시다.
    • 캐릭터가 왼쪽으로 너무 많이 가서 계산된 newPosition.x -10이 되었다면, Mathf.Clamp는 이 값을 -9로 바꿔서 돌려줍니다.
    • 캐릭터가 오른쪽으로 가서 newPosition.x 12가 되었다면, 9로 바꿔줍니다.
    • 캐릭터가 화면 안에서 움직여 newPosition.x 5라면, 그대로 5를 돌려줍니다.
    결과적으로 캐릭터의 x좌표는 절대 -9보다 작아지거나 9보다 커질 수 없게 됩니다.

 

 

>이건 걍 조건문 써도될것같은데

굳이 메소드 이름 모르면 억지로 메소드 쓸필요없이 

 


2번 개념: ScreenToWorldPoint (스크린 좌표를 월드 좌표로)

"번역기" 라고 생각하시면 가장 쉽습니다.

문제 상황:

  • 내 캐릭터는 게임 세상(World) 에 살고 있습니다. 캐릭터의 위치(transform.position)는 게임 세상의 좌표(예: x=5.4, y=-2.1)로 표시됩니다.
  • 하지만 우리는 캐릭터가 "게임 화면(Screen)" 밖으로 나가지 않기를 바랍니다. 게임 화면의 좌표는 픽셀 단위입니다. (예: 왼쪽 아래는 0,0 픽셀)

이 둘은 서로 사용하는 언어(좌표계) 가 다릅니다. 캐릭터에게 "너 0번 픽셀 밖으로 나가지 마!" 라고 말해봤자 알아듣지 못합니다.

해결책 (번역기):
Camera.main.ScreenToWorldPoint() 라는 번역기가 필요합니다.

이 번역기는 "스크린의 이 픽셀은, 게임 세상의 어느 좌표에 해당하나요?" 를 알려줍니다.

C#
// "번역기야, 스크린의 왼쪽 아래 모서리(0, 0 픽셀)가
// 게임 세상에서는 무슨 좌표인지 번역해줘!"
Vector2 bottomLeft = cam.ScreenToWorldPoint(new Vector3(0, 0, ...));

이 코드가 실행되면, bottomLeft라는 변수에는 (x: -8.8, y: -5.0) 와 같이 캐릭터가 알아들을 수 있는 게임 세상 좌표가 저장됩니다.

이제 우리는 캐릭터에게 이렇게 말할 수 있습니다.
"너의 x 위치는 -8.8 보다 작아지면 안 돼!"
캐릭터는 이 말을 알아듣고 그 위치를 넘어가지 않게 됩니다.

결론: ScreenToWorldPoint 눈에 보이는 화면의 경계선을, 캐릭터가 실제로 사용하는 게임 세계의 좌표로 "번역" 해주는 아주 중요한 기능입니다.


3번 개념: new Vector2(horizontal, vertical)

"조이스틱" 이라고 생각하시면 완벽하게 이해됩니다.

상황:

  • horizontal 변수에는 좌우 움직임 값만 들어있습니다. (A키 = -1, D키 = 1)
  • vertical 변수에는 상하 움직임 값만 들어있습니다. (S키 = -1, W키 = 1)

이 두 개는 각각 별개의 정보입니다. "오른쪽으로", "위쪽으로" 라는 단일 방향만 알려주죠.
하지만 우리는 "오른쪽 위 대각선으로" 움직이고 싶을 때도 있습니다.

해결책 (조이스틱):
new Vector2(x, y)는 이 두 개의 분리된 정보를 합쳐서 하나의 "방향 막대기(벡터)" 로 만들어 줍니다. 이 막대기는 조이스틱처럼 기울어진 방향을 나타낼 수 있습니다.

  • D키만 누를 때:
    • horizontal = 1, vertical = 0
    • new Vector2(1, 0) => 오른쪽으로만 기울어진 조이스틱 (→)
  • W키만 누를 때:
    • horizontal = 0, vertical = 1
    • new Vector2(0, 1) => 위쪽으로만 기울어진 조이스틱 (↑)
  • D키와 W키를 동시에 누를 때:
    • horizontal = 1, vertical = 1
    • new Vector2(1, 1) => 오른쪽 위 대각선으로 기울어진 조이스틱 (↗)

이렇게 만들어진 moveDirection이라는 조이스틱(방향 막대기)을 캐릭터의 위치에 더해주면, 캐릭터는 그 조이스틱이 가리키는 방향으로 자연스럽게 움직이게 됩니다.

결론: new Vector2(horizontal, vertical) 좌우, 상하의 개별적인 입력을 "하나의 통합된 방향"으로 만들어주는 과정입니다. 이렇게 해야 대각선 이동을 포함한 모든 방향으로 부드럽게 움직일 수 있습니다.

 

 

 


cam.nearClipPlane은 왜 필요한거지? 그냥 Screen.width, Screen.height만 있으면 왜 안되지, 

또한 현재 2d게임인데

 

핵심 요약: 유니티는 "가짜 2D" 입니다.

가장 중요한 사실은, 유니티는 근본적으로 3D 엔진이라는 것입니다. 우리가 '2D 모드'로 프로젝트를 만들어도, 실제로는 3D 공간에서 Z축을 고정하고 카메라를 원근감 없는 방식(Orthographic)으로 비추어서 2D처럼 보이게 만드는 것입니다.

모든 게임 오브젝트는 여전히 보이지 않는 Z축 좌표를 가지고 있습니다. (보통 2D 게임에서는 캐릭터, 배경 등을 Z=0에 두고, 카메라는 Z=-10에서 그것들을 바라봅니다.)


cam.nearClipPlane이 꼭 필요한 이유: "깊이(거리)" 정보

ScreenToWorldPoint 함수는 "스크린의 2D 픽셀 좌표"를 "게임 세상의 3D 좌표"로 바꾸는 번역기라고 말씀드렸습니다.

여기서 발생하는 근본적인 문제를 비유로 설명해 드릴게요.

당신이 창문 앞에 서서, 창문 유리에 매직으로 점(●)을 하나 찍었다고 상상해 보세요.
그리고 친구에게 "저 점이 가리키는 방 안의 실제 물건이 뭐야?" 라고 묻습니다.

이 질문은 대답할 수가 없습니다. 왜냐하면 그 점 뒤로는 무한한 공간이 있기 때문입니다.

  • 그 점 바로 뒤에 있는 벽지일 수도 있고,
  • 방 건너편에 있는 책상일 수도 있고,
  • 더 멀리 있는 책장의 책일 수도 있습니다.
<br>
<br>

Screen.width, Screen.height 같은 X, Y 좌표는 창문 유리의 어느 지점에 점을 찍을지 알려주는 것과 같습니다. (방향)

하지만 그 점에서부터 얼마나 깊이(거리) 들어가야 하는지를 알려주지 않으면, 유니티는 무한한 가능성 중에서 어떤 지점의 좌표를 알려줘야 할지 알 수 없습니다.

cam.nearClipPlane가 바로 이 "깊이(거리)" 정보를 제공하는 역할을 합니다.

  • new Vector3(0, 0, cam.nearClipPlane) 의 의미:
    • 0, 0: 스크린의 왼쪽 아래 모서리 (방향)
    • cam.nearClipPlane: 카메라로부터 가장 가까운 거리(깊이) 에 있는 지점

즉, "카메라에서 레이저를 쏘는데, 스크린의 왼쪽 아래를 통과해서, 카메라가 볼 수 있는 가장 가까운 평면에 닿는 바로 그 지점의 월드 좌표를 알려줘!" 라는 아주 구체적인 명령이 되는 것입니다.

결론

  • Screen.width, Screen.height만으로는 안 되는 이유:
    X, Y값은 변환할 **'방향'**만 알려줄 뿐, 3D 공간에서의 '깊이(거리)' 정보를 담고 있지 않기 때문입니다. 깊이가 없으면 변환할 하나의 특정 지점을 정할 수 없습니다.
  • cam.nearClipPlane이 필요한 이유:
    ScreenToWorldPoint라는 3D 함수에게 "카메라로부터 이만큼 떨어진 거리에서 좌표를 계산해줘"라고 명확한 '깊이(거리)' 기준을 알려주기 위해 필요합니다. nearClipPlane은 카메라에 기본적으로 설정된 값이라 사용하기 편리한 기준점일 뿐입니다. (다른 Z값을 넣어도 되지만, 보통은 이 값을 사용합니다.)

이 개념은 처음에는 조금 어렵지만, "2D 게임도 사실은 3D 공간 위에서 돌아간다" 라는 점만 기억하시면 훨씬 이해하기 쉬우실 겁니다.

 

결론부터 말씀드리면, 대부분의 2D 게임에서는 세 번째 매개변수로 cam.nearClipPlane을 그냥 공식처럼 써주시면 됩니다.


Cannot implicitly convert type 'float' to 'UnityEngine.Vector2'CS0029
(field) int RubyController.speed
        Vector2 position = transform.position;
        //position.x = position.x + movingMagicNumber*speed*Time.deltaTime;
        position = horizontal * speed;
        transform.position = position;

ㄴㄴ

position.x = horizontal * speed;

 

 

 


 

이렇게 짯는데, 위로 가다가 막힘 왜지?

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RubyController : MonoBehaviour
{
    public int speedX=1;
    public int speedY = 1;
    //float movingMagicNumber = 0.01f;

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {   
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");
        Vector2 position = transform.position;
        //position.x = position.x + movingMagicNumber*speed*Time.deltaTime;
        position.x = horizontal * speedX;
        position.y = vertical * speedY;
        transform.position = position;
    }
}
        position.x += horizontal * speedX;
        position.y += vertical * speedY;

할당말고

+=을 해야지 

 

 


QualitySettings.vSyncCount = 0;
Application.targetFrameRate = 10;

1. QualitySettings.vSyncCount = 0;

  • 한 줄 요약: "모니터 속도에 맞추지 말고, 우리 게임 마음대로 속도를 조절할게!" 라는 선언입니다.
  • 쉬운 비유:
    대부분의 모니터는 1초에 60번 깜빡입니다 (60Hz). vSyncCount는 게임 그림책(FPS)을 넘기는 속도를 이 모니터의 깜빡이는 속도에 강제로 맞추는 기능입니다.
    • vSyncCount = 1 (기본값): "모니터가 60번 깜빡이니, 우리도 그림책을 1초에 딱 60장만 넘기자!" (60 FPS로 고정됨)
    • vSyncCount = 0 : "모니터는 신경 쓰지 마! 우리가 알아서 그림책 넘기는 속도를 정할 거야!" (속도 제한 해제)
    이 코드는 두 번째 줄의 명령어를 듣게 하기 위한 사전 작업입니다. 이걸 끄지 않으면 두 번째 줄 코드가 무시될 수 있습니다.

2. Application.targetFrameRate = 10;

  • 한 줄 요약: "우리 게임 그림책은 1초에 딱 10장만 넘겨!" 라고 속도를 직접 지정하는 명령어입니다.
  • 쉬운 비유:
    바로 위에서 vSyncCount = 0으로 "우리 마음대로 속도를 정할게!" 라고 선언했습니다. 이 코드는 그 '마음대로 정한 속도'가 얼마인지를 구체적으로 알려주는 것입니다.
    • targetFrameRate = 60 : "1초에 그림 60장씩 넘겨줘!" (부드러운 움직임)
    • targetFrameRate = 10 : "1초에 그림 딱 10장씩만 넘겨줘!" (뚝뚝 끊기는 움직임)

 

게임이 초당 60 프레임으로 실행되는 경우 루비는 0.1 * 60으로 이동하므로 초당 6 유닛만큼 이동하는 것입니다.

하지만 방금 한 것처럼 게임이 초당 10 프레임으로 실행되면 루비는 0.1 * 10으로 이동하여 초당 1 유닛만큼 이동하게 됩니다.

 

아 deltaTime설명하려고 있는코드구나