게임 엔진/Unity

Unity 3일차 - C# 스크립트 기초 -2

FakeZero 2021. 6. 9. 04:34

  자 저번회차에 이어서 C# 스크립트의 기초를 배워봅시다...는 오늘로 마지막입니다.

  그리고 하나더 갑자기 빡센 일정이 하나 들어왔습니다. 그러니 적어도 9월까지는 3D 강의까지 강의를 작성하려 합니다. 그러니 느슨하게 가는 것은 오늘이 마지막이라고 보셔도 좋을 것 같습니다 ^^

 

  오늘만 잘 버티시면 다음주부터는 지루한 텍스트 코딩을 마치고 직접 게임 엔진을 다루러 갑니다. 그러니 오늘 강좌도 잘 보고 따라오시길 바랍니다.

 


1. 메서드

우린 이미 메서드에 대한 내용을 어느정도 알고 있습니다. 강좌 첫시간에 잠깐 설명했기 때문이죠. C# 스크립트를 생성하면 자동으로 생성되는 Start 메서드와 Update 메서드. 우린 이미 이 두 개의 메서드를 잘 사용하고 있습니다.

 

그렇다면 오늘은 메서드를 직접 만들어보도록 합시다.

사실 말만 다르지 메서드는 C언어에서의 함수와 같은 역할을 합니다. 이름을 정해주고, 필요한 변수가 있다면 함수에 넣을 수 있도록 변수를 생성하고, 실행문을 만들어서 결과적으로 프로그램 코드에서 중복되는 코드들을 간략하게 바꾸어 주죠.

일단 간단하게 Hello, World를 출력하는 메서드를 만들어볼까요?(아마 이제 C# 스크립트를 수정하는 방법은 아리라 믿습니다.)

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

public class Test : MonoBehaviour
{
    void Hello()
    {
        Debug.Log("Hello, World.");
    }
 
    void Start()
    {
        Hello();
    }
}

이렇게 Hello라는 새로운 메서드를 만들었습니다.

새로운 메서드를 만들때는 Start 메서드처럼

|자료형| |메서드 이름|(필요한 변수 선언)
{
	|실행문|
}

필요한 자료형을 쓰고 메서드 이름과 변수가 필요하다면 변수를 선언해주고 블록안에 실행문을 써주시면 됩니다.

위의 코드 같은 경우는 Hello라는 메서드를 만들었습니다.

직접 내보낼 데이터가 없으니 |자료형| 부분엔 void를 써 주었습니다.

변수가 필요없으니 변수 선언은 하지 않아도 되구요.

실행문에는 콘솔에서 Hello, World가 출력되도록 만들었습니다.

 

이렇게 만들어진 메서드를 Start 메서드에서 그대로 쓸 수 있습니다. Start 메서드에서 Hello 메서드를 사용한 후, 프로그램을 Unity에서 실행하면 Unity의 콘솔창에서 "Hello, World."가 출력될 것입니다.

 

메서드를 작성할 때는 해당 메서드에 필요한 변수가 필요할 수도 있고 필요하지 않을 수도 있습니다. Hello 메서드의 경우 단순히 정해진 문자열을 출력하는 메서드이므로 변수가 필요하지 않아서 비워두었습니다. 실행할 때에도 Hello()와 같이 괄호 안을 비워두었습니다. 그러면 변수가 필요하면 어떻게 해야할까요?

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

public class Test : MonoBehaviour
{
    int Add(int a, int b)
    {
        int c = a + b;
        return c;
    }

    void Start()
    {
        int answer = Add(2, 3);
        Debug.Log(answer);
    }
    
}

 

이번엔 정수 자료형을 더해주는 Add라는 메서드를 만들어봤습니다.

정수 자료형을 내보낼 것이기 때문에 이번엔 int 메서드입니다. 이름은 Add이구요.

이번엔 더할 두 변수가 필요하기 때문에 a와 b라는 변수를 만들어 줬습니다.

블록 안에는 새로 c라는 변수를 만들어서 a와 b를 더한 값을 대입해 줬습니다. 이때 c라는 변수는 Add 메서드 안에서 선언된 변수이므로 다른 메서드에서는 사용할 수 없습니다.

변수 c를 선언한 후에는 return을 통해 c의 값을 반환해줬습니다.

 

Start 메서드를 봅시다.

answer라는 변수에 Add 메서드를 써서 2와 3을 더한 값을 반환하도록 해주었습니다. 위 코드에서는 2와 3이므로 answer 변수에는 5라는 값이 들어간 셈입니다.

마지막으로 Debug.Log를 통해 Unity 콘솔창에 answer 변수에 저장된 값을 출력합니다. 당연히 5가 출력될 것입니다.

 

이렇게 메서드를 만들어봤습니다. 정수뿐만 아니라 메서드는 다양한 자료형을 내보내고 받을 수 있습니다.

 


2. 클래스

클래스에 대한 것도 우리는 알고 있는게 하나 있습니다. 맞습니다. 우리가 지금 프로그램을 하고 있는 공간. C# 스크립트의 이름으로 되어 있는 클래스에서 우리는 코드를 작성하고 있죠.

이번엔 이 클래스를 직접 한 번 만들어봅시다.

 

클래스는 쉽게 말하면 상자입니다. 위에서 배운 메서드와 변수들을 모아서 정리할 수 있는 상자라고 보면 편할 것입니다. 만약 메서드가 비슷한 일을 한다면 한 클래스 안에 모아두었다가 필요할 때 클래스 안에서 필요한 메서드를 꺼내서 쓸 수 있도록 해주는 것이 가능합니다.

우리가 주로 사용하는 자료형에는 정수형(int), 실수형(float, double), 문자열(string)등이 있을 겁니다. 클래스를 만든다는 것은 우리가 원하는 새로운 형태의 자료형을 만든다고도 할 수 있습니다. 에잉? 자료형을 만든다고? 이게 대체 무슨 소리지? 라고 생각할 수 있습니다. 그럼 이게 대체 무슨 소리인지 클래스에 대해서 배워보도록 합시다.

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


public class Player
{
    private int hp = 100;
    private int power = 50;

    public void Attack()
    {
        Debug.Log("내 공격력:" + this.power);
    }

    public void Damage(int damage)
    {
        this.hp -= damage;
        Debug.Log(damage + "데미지를 입었다.");
        Debug.Log("체력이 " + this.hp + "만큼 남았다.");
    }
}

public class Test : MonoBehaviour
{
    void Start()
    {
        Player myplayer = new Player();

        myplayer.Attack();
        myplayer.Damage(30);
    }
}

갑자기 꽤나 긴 코드가 튀어나왔습니다. 하지만 여러분은 이제 이런 긴 코드에 익숙해져야 합니다. 그리고 이런 코드속에서 원하는 정보를 알아낼 수 있는 역량을 키워야만 합니다. 그러니 한 번 하나하나 분석해 보겠습니다.


분석을 시작하기에 앞서 딱 봤을 때 우리가 평소에 다루지 않던 키워드가 갑자기 많이 등장하는 것을 볼 수 있습니다. 바로 public과 private 키워드입니다. 이 두 키워드는 접근 수식자라고 합니다. 접근 수식자에는 다음과 같은 종류가 있습니다.

접근 수식자 접근 가능 클래스
public 모든 클래스에서 접근 가능
protected 같은 클래스와 해당 클래스의 서브 클래스에서 접근 가능
private 같은 클래스에서만 접근 가능

즉, 접근 수식자는 만든 클래스나 메서드, 변수에 대한 다른 클래스에서의 사용 가능 여부를 결정해 주는 것입니다....만  접근 수식자의 역할은 한 가지 더 존재합니다.

바로 내가 만든 프로그램을 쓰는 다른 프로그래머들에게 수정 가능 여부를 알리는 것 입니다. 예를 들어 private 접근 수식자가 붙어있는 변수는 변경하지 말라고 의사 표시를 하는 것이고, 메서드의 경우에는 사용하지 말라고 하는 간접적인 표현을 할 수 있는 것입니다.

물론 이러한 의사 표현은 주석을 통해 할 수도 있습니다. 그러나 그러한 코드는 가독성이 떨어지고 깨끗하지 못합니다. 그러니 가능하면 이러한 기능들을 사용해 다른 사용자들에게 자신의 의사표시를 할 수 있는 방법을 알아두는 것이 좋습니다.


이제 본격적으로 코드를 분석해보도록 하죠.

 

현재 이 코드에는 두 가지의 클래스가 존재합니다. 우리가 평소에 코드를 작성하는 Test 클래스와 새로 만들어진 Player 클래스입니다.

public class Player
{
    private int hp = 100;
    private int power = 50;

    public void Attack()
    {
        Debug.Log("내 공격력:" + this.power);
    }

    public void Damage(int damage)
    {
        this.hp -= damage;
        Debug.Log(damage + "데미지를 입었다.");
        Debug.Log("체력이 " + this.hp + "만큼 남았다.");
    }
}

Player 클래스부터 분석하겠습니다.


private int hp = 100;
private int power = 50;

일단 두 개의 변수가 선언 되었습니다.

hp라는 변수에는 100, power라는 변수에는 50이 대입되어 있으며, private 접근 수식자가 붙어있으므로 이 두 변수는 Player 클래스에서만 사용할 수 있습니다.


이번엔 메서드입니다. Player 클래스 안에는 두 개의 메서드가 있습니다.

public void Attack()
{
	Debug.Log("내 공격력:" + this.power);
}

Attack 메서드는 power 변수를 출력하는 역할을 합니다....어라? power 변수 앞에 이상한 키워드가 붙어있습니다?

바로 this 키워드입니다. this 키워드는 현재 실행문이 들어있는 클래스를 의미합니다. 사실은 좀 다른 의미이지만 지금은 이렇게 이해하도록 합시다.

그리고 power 변수를 출력할때 this옆에 .이 붙어있는 걸 볼 수 있습니다. 이 .은 연결해주는 역할이라고 볼 수 있습니다. 위의 코드로 예를 들면 this.power은 Player 클래스의 power 변수를 불러오라는 뜻입니다.

사실 power변수는 this 키워드를 쓰지 않아도 불러올 수 있습니다. 그런데 왜 굳이 this 키워드를 쓸까요?

알아보기 쉽도록 코드를 다음과 같이 바꿔보겠습니다.

public void Attack()
{
	int power = 30;
	Debug.Log("내 공격력:" + power);
}

이번엔 Attack 메서드 안에 새로운 power 변수를 만들었고 power 변수를 불러올 때 this 키워드를 제거했습니다.

이렇게 되면 Player 클래스 안에는 두 개의 power 변수가 존재하게 됩니다. Attack 메서드 안에 있는 power 변수와 밖에 있는 power 변수입니다.

이렇게 된 경우 Attack 메서드에서 출력되는 power 변수는 Attack 메서드의 안에 있는 power 변수 입니다. 따라서 50이 아닌 30이 출력됩니다.

this 키워드는 바로 이러한 상황 때문에 존재합니다. 이름이 같은 변수가 존재할 때에 그 변수를 사용하면 프로그램은 변수를 사용하는 코드로부터 가장 가까이 있는 변수를 우선적으로 사용합니다. 즉, 위 코드에서는 가장 가까이 있는 power 변수인 30이 대입된 power 변수를 출력하는 것입니다.

public void Attack()
{
	int power = 30;
	Debug.Log("내 공격력:" + this.power);
}

자, 이제 this 키워드를 다시 붙여줬습니다. 이 경우에는 Attack 메서드 밖에 있는, 즉, Player 클래스에 진입하자마자 존재하는 power 변수를 사용하게 되서 이제는 다시 50을 출력하게 됩니다.

이렇게 변수나 메서드를 사용할 때에는 어디에 있는가가 상당히 중요합니다. 그러니 원하는 변수나 메서드, 클래스를 사용할 때에는 사용할 변수, 메서드, 클래스의 위치를 정확히 프로그램에게 알려주어야 합니다.

지금까지의 설명으로 Attack 메서드는 [내 공격력:50]을 출력하는 메서드라는 것을 알 수 있습니다.


public void Damage(int damage)
{
	this.hp -= damage;
	Debug.Log(damage + "데미지를 입었다.");
	Debug.Log("체력이 " + this.hp + "만큼 남았다.");
}

이번엔 Damage 메서드를 살펴보겠습니다. 조금 둘러봐도 알겠지만 해당 메서드는 데미지 계산을 하는 메서드임을 바로 알 수 있을 것입니다.

Damage 메서드에서는 변수가 필요하므로 damage라는 정수형 변수가 선언되어 있습니다.(참고로 프로그램은 대문자와 소문자를 다르게 인식하므로 Damage와 damage는 다른 이름입니다.)

this.hp -= damage;

이 코드가 하는 일은 이미 전시간에 배웠습니다 Player 클래스의 hp에서 damage의 값만큼 빼준다는 의미를 가지고 있습니다.

Debug.Log(damage + "데미지를 입었다.");
Debug.Log("체력이 " + this.hp + "만큼 남았다.");

이 코드들도 특별한게 없습니다. Damage메서드를 쓸 때 입력받은 damage와 Damage 메서드에 의해 변형된 hp를 출력하는 것입니다.


public class Test : MonoBehaviour
{
    void Start()
    {
        Player myplayer = new Player();

        myplayer.Attack();
        myplayer.Damage(30);
    }
}

이번엔 Test 클래스를 살펴봅시다.

아마 전부터 궁금하던 사람도 있을 것입니다. Test가 클래스 이름이라는 건 알겠는데 그 옆에 있는 :MonoBehaviour은 대체 뭐하는 녀석인가에 대해서 말입니다. 결론부터 얘기하면 MonoBehaviour 또한 클래스로 Unity에서 준비해둔 클래스입니다.

:는 MonoBehaviour 클래스를 Test 클래스에서 사용할 수 있도록 상속 시킨다, 즉, Monobehaviour 클래스의 기능을 Test 클래스에 집어 넣겠다는 뜻입니다.


void Start()
{
	Player myplayer = new Player();

	myplayer.Attack();
	myplayer.Damage(30);
}

이제 마지막으로 Start 메서드까지 알아보면 코드 분석이 끝납니다. 그런데 시작부터 이때까지 보지 못했던 녀석이 나옵니다.

Player myplayer = new Player();

이 코드는 우리가 생성한 Player 클래스를 Test 클래스의 Start 메서드에서 사용할 수 있도록 선언해주는 것이라고 할 수 있습니다. 위에서 제가 클래스를 만든다는 것은 자료형을 만든다는 것과 비슷한 것이라고 설명했습니다. 그렇기 때문에 이 코드는 myplayer이라는 변수를 Player이라는 자료형으로 만들어주는 것이라고 볼 수 있습니다.

이렇게 클래스를 통해 만들어진 변수안에 들어가는 클래스의 실체를 인스턴스라고 합니다. 위에서 제가 this 키워드에 대해서 설명할 때 this 키워드는 현재 실행문이 들어있는 클래스를 의미한다고 얘기했습니다만 이제야 this의 정확한 의미를 말할 수 있겠군요. this 키워드는 정확히는 클래스 자체가 아닌 생성된 자신의 인스턴스를 지정하는 키워드입니다.

우리가 만든 클래스를 사용하기 위해서는 이렇게 인스턴스를 만들어서 사용해야합니다.

|클래스 이름| |인스턴스 이름| = new |클래스 이름|(|필요한 변수값|);

위와 같은 형식으로 인스턴스를 생성할 수 있습니다. 정확히는 new |클래스 이름|(|필요한 변수값|)부분이 인스턴스 입니다. 이렇게 인스턴스를 만들어주어야 비로소 우리는 클래스를 사용할 수 있습니다. Player 클래스는 아무런 변수를 필요로 하지 않으므로 |필요한 변수값| 부분에는 아무것도 들어가지 않습니다.

myplayer.Attack();
myplayer.Damage(30);

이 코드들은 생성된 인피던스를 통해 클래스 안에 있는 메서드를 사용하는 것입니다. myplayer의 인피던스는 Player 클래스를 사용하고 있으므로 이 코드들은 Player 클래스의 Attack, Damage 메서드를 사용하는 코드들인 것입니다.

myplayer.Attack()은 당연히 [내 공격력:50]이 출력되는 것을 알 수 있습니다.

myplayer.Damage(30)은 30이라는 값이 damage 변수에 들어갔으므로 [30데미지를 입었다.], [체력이 70만큼 남았다.]라고 출력되는 것을 볼 수 있을 것입니다.


자 이렇게 해서 클래스의 사용법을 한 예제를 통해 쫙 알아봤습니다. 차근차근하니 별로 어렵지 않죠? 오늘 한 것처럼 어떤 상황에도 침착함과 냉정함을 유지하며 현 상황을 분석하는 힘이 프로그래머에게는 필요한 역량이 될 것입니다.


3. Vector 클래스

클래스를 배웠으니 마지막으로 Unity에서 사용할 수 있는 특별한 클래스에 대해서 배워봅시다. 바로 Vector(벡터) 클래스입니다. 네. 기하와 벡터의 그 벡터 맞습니다. 게임은 그래픽적 요소가 동반되는 매체인 만큼 벡터에 대한 지식은 필수라고 할 수 있습니다.

 

잠깐 벡터가 무엇인지 알아보도록 할까요? 벡터는 단순히 값만을 가지지 않습니다. 일정한 값과 방향을 같이 가지는 것이 바로 벡터입니다. 예를 들어 단순히 '3'이라는 숫자가 있으면 이것은 단순히 값(이러한 크기만을 가지는 값을 스칼라라고 합니다.) 그러나 벡터 값으로 3은 방향도 명시해 줘야하므로 '+x의 방향으로 3' 같은 형태로 표현합니다.

<2D에서의 방향>

2D(2차원)에서는 두 가지 방향을 제시 할 수 있습니다. x축 방향과 y축 방향, 즉, 가로와 세로 입니다. 따라서 이 두 가지 방향을 조합해 사선을 만드는 것도 가능합니다.

<3D에서의 방향>

3D(3차원)에서는 2D에서보다 축이 하나 더 늘어납니다. 따라서 원하는 모든 방향을 벡터로 표현할 수 있게 됩니다.

 

자 이제 벡터에 대해서 알았으니 Vector 클래스를 직접 사용해봅시다.

Unity에서 제공하는 Vector 클래스는 2가지가 있습니다. 2D를 지원하는 Vector2 클래스와 3D를 지원하는 Vector3 클래스입니다.

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

public class Test : MonoBehaviour
{
    void Start()
    {
        Vector2 playerPos = new Vector2(3.0f, 4.0f);

        playerPos.x += 8.0f;
        playerPos.y += 5.0f;
        Debug.Log(playerPos);
    }
}

위 예제를 살펴보도록 하죠.

위 코드를 보면 숫자 다음에 f가 붙어있는 것을 알 수 있습니다. 이로서 Vector 클래스는 float 자료형을 쓴다는 것을 알 수 있습니다. 따라서 값을 집어넣어 줄때는 꼭 f를 붙여주어야 합니다.

Vector2 playerPos = new Vector2(3.0f, 4.0f);

저희는 2D 프로젝트를 만들었으므로 Vector2 클래스를 사용하겠습니다.

위 코드를 통해 인피던스를 생성했습니다. 이때 괄호 안에 3.0f, 4.0f를 넣어서 초기값을 지정해 주었습니다.

class Vector2
{
    public float x;
    public float y;
    
    //이 이후는 Vector2의 메서드들
}

사실은 더 복잡하지만 Vector2 클래스는 위와 같은 형식으로 되어 있습니다. 그렇기에 초기값을 지정해 줄 수도 있고 Vector2의 자료형으로 만든 인피던스에서 x, y 변수의 값을 가져다 쓸 수 도 있습니다.

playerPos.x += 8.0f;
playerPos.y += 5.0f;
Debug.Log(playerPos);

이 세 코드들은 Vector2 자료형으로 만들어진 PlayerPos라는 변수의 인피던스에 x와 y 변수에 변화를 주고 그것을 출력하는 역할을 합니다.

PlayerPos.x += 8.0f는 방금 지정해 준 x 초기값인 3.0에 8.0을 더해 11.0으로 만들어 주었습니다.

PlayerPos.y += 5.0f는 y 초기값인 4.0에 5.0을 더해 9.0으로 만들어 주었습니다.

그리고 Debug.Log(playerPos)를 통해 playerPos의 값을 출력하면 (11.0, 9.0)이라고 출력됩니다.

 

또한 기하와 벡터를 배운 사람들은 알겠지만 벡터는 벡터끼리 연산이 가능합니다. Unity에서도 예외는 아닙니다.

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

public class Test : MonoBehaviour
{
    void Start()
    {
        Vector2 startPos = new Vector2(2.0f, 1.0f);
        Vector2 endPos = new Vector2(8.0f, 5.0f);
        Vector2 dir = endPos - startPos;
        Debug.Log(dir);

        float len = dir.magnitude;
        Debug.Log(len);
    }
}

위 코드에서는 startPos의 초기값(좌표)를 (2.0, 1.0)으로 설정해 두었고 endPos의 벡터를 8.0, 5.0으로 설정해 두었습니다.

이어서 dir이라는 Vector2형 변수에는 endPos 좌표에서 startPos 좌표가 빠진 벡터값이 들어가게 됩니다.

그러므로 dir을 출력하게 되면 (8.0-2.0, 5.0-1.0)=(6.0, 4.0)이라고 출력됩니다.

 

그 밑에 있는 float형 변수 len은 magnitude 키워드를 이용해 startPos부터 endPos까지의 거리를 구할 수 있습니다. 이 값은 모두가 잘 아는 피타고라스의 정리를 기반으로 구해지며 위 코드 같은 경우 7.211102가 출력됩니다.

 

이렇게 해서 Vector 클래스를 살펴봤습니다. 눈치가 빠른 사람들은 우리가 게임을 만들때 오브젝트의 이동을 이 Vector 클래스를 이용한다는 것을 알 수 있을 껍니다.


이로써 C# 스크립트의 기초가 끝났습니다. 다음주부턴 드디어 게임엔진을 다루겠습니다. 질문이 있으면 댓글로 질문 바랍니다.

 

수고하셨습니다.