본문 바로가기

Starters 부트캠프/B - log

유데미 스타터스 유니티 개발자 취업 부트캠프 1기 - 4주차 학습 일지 / 유데미 [C#과 Unity로 3D 게임 개발하기] 강의 리뷰

3주 차에 공부를 하면서 가장 크게 느꼈던 점은 내가 생각보다 모르고 넘어간 기본 개념들이 많다는 점이었다. 

아무래도 그동안 만들고 싶은 것을 완성하는 데에만 정신이 팔려 가져온 코드들이, 클론 코딩한 코드들이 어떻게 돌아가고, 어떤 내용을 담고 있는지 제대로 알아보려 하지 않았던 태도 때문임이 분명했다. 

그래서 이번 교육에서 배운대로만 쓰는 것이 아니라 다른 기능으로도 활용할 수 있도록 코드를 흡수하려고 노력하고 있다.

 

이번 주 교육에서는 유니티 개발 교육에 들어가기 앞서 C# 개념들을 마저 배웠다.

그리고, 다음 주부터 게임을 만드는 것에 대비해서 유데미와 다른 블로그 등에서 제공하는 강의들을 듣고 스스로 자습하는 시간도 가졌다.

그러다 보니 수업뿐 아니라 자습으로도 배운 것들이 많아서 이번 B-Log는 크게 수업의 내용과 자습 내용을 정리하는 것으로 채우려고 한다.

 


 

Starters 부트캠프 4주 수업 내용 -  C# 객체지향 심화 등

 

Sealed 한정자

public, private, protected 등의 한정자처럼 Sealed 역시 한정자로, 특이하게도 하위 클래스가 해당 클래스를 상속받지 않기를 원할 경우에 사용한다.

 

public sealed class Seal : MonoBehavior
{
	public virtual void SealedMethod()
	{
    
	}
}

public class UnderSeal : Seal
{
	
}

// : Seal Error

 

물론 클래스 전체가 아니라 특정 메서드만 막고 싶을 수도 있다. 

그럴 경우는 메서드에 sealed 한정자를 붙이면 된다. (단, virtual 한 메서드는 되지 않는다고 하셨다.)

 

public class Seal : MonoBehavior
{
	public sealed void SealedMethod()
	{
    
	}
}

public class UnderSeal : Seal
{
	SealedMethod();	// 사용은 가능
    
	public new void SealedMethod()	// 재정의는 불가 
	{
    
	}
}

// : Seal Error

 

참고로 아래에 Static 클래스의 경우는 애초에 sealed가 설정되어 있다.

 

정적 클래스와 정적 메서드

정적 클래스(메서드)는 항상 단일 존재하에 생성되어 관리된다. 

그래서 이를 사용하는 한 클래스에서 해당 클래스를 변형하면 다른 곳에서 사용할 때에도 변화된 값을 받게 된다. 

이런 정적 클래스, 메서드는 주로 변환을 해주는 클래스나 Helper 클래스처럼 유틸리티적인 처리를 행하는 클래스(메서드)에 사용된다.

 

다만, 사용에 있어 주의해야 하는 점이 몇 가지 있다.

1. 단일 존재이므로 인스턴스화가 불가능
2. 정적 클래스 안에는 정적 멤버들만 포함
3. 애초에 sealed가 설정되어 있음

 

public static class Converter
{   
    public static int ReturnFloor(double d)
    {
        return (int)d;
    }
}

// 정적 클래스 선언
// 정적 클래스는 정적 멤버만 포함

 

다른 클래스에서 사용할 때에는 Converter.ReturnFloor(doubleV); 이런 식으로 사용하면 된다.

- 기존처럼 생성자를 이용해서 인스턴스화 하여 사용하면 안 됨

 

정적 메서드의 경우에는 신기하게도 정적 클래스가 아닌 클래스에서도 선언할 수 있다. 

이런 경우, 해당 메서드는 그 클래스 내에서 단일로 생성이 되고, 다른 클래스에서는 여러 번 인스턴스 화가 가능하다.

다만, 인스턴스로 한번 해당 메서드를 수정하면, 원본 메서드의 내용까지 같이 바뀌게 된다.(단일 존재이므로)

 

게임에서는 정적 클래스를 '싱글톤'이라는 디자인 기법으로 활용하는데 이는 자습 필기 정리에서 다시 살펴보겠다.

 

추상화

해당 클래스의 메서드를 동시에 여러군데에서 쓰는 경우가 있다.

예를 들면 Animal이라는 클래스의 dog, cat, rabbit, tiger 등의 클래스는 모두 eat라는 메서드를 사용하게 된다.

하지만, rabbit은 초식만 하는 반면, tiger는 육식을 한다는 등의 차이가 발생하게 되는데, 이런 문제가 생기지 않게끔 우리는 해당 eat메서드에 내용을 다 정하지 않고 설계도만 전달할 수 있다. 그것이 추상화이다.

 

조금 더 유명한 예로는 '죽음의 다이아몬드'라는 예가 있다.

게임 상에서 크리쳐라는 클래스에 아쳐 클래스와 몬스터 클래스가 있는데, 우리가 새롭게 야쳐이면서 몬스터인 새로운 클래스를 만들고 싶다고 치자.

그래서 새롭게 아쳐 몬스터 클래스를 만들고, 아쳐와 몬스터 클래스를 상속시켰는데, 공격 행동에서 문제가 발생한다.

공격을 실행하려고 하는데, 아쳐에도 Attack() 메서드가 있고, 몬스터에도 Attack() 메서드가 있는 것이다. 

과연 어떤 Attack()으로 실행이 되어질까?

 

아쳐 : "자고로 내 자식이면, 사람처럼 활로 공격해야 한다."
몬스터 : "아니, 내 자식이라면, 물어뜯는 식으로 공격해야 맞지."

아쳐 몬스터 : "나는 어떻게 해야 하는가?"

 

일단 C#에서는 애초에 이런 경우를 미연에 방지하고자 일반 클래스들을 여럿 상속할 수 없도록 해놓았다.

그럼에도 이런 문제는 충분히 발생할 수 있기 때문에 우리는 추상화 클래스를 통해 설계도를 제시할 수 있다.

 

public abstract class AbstractClass
{
    public abstract double SetDamage();

    public abstract double SetDamageProp
    {
        // int a = 0;
        // … → 오류 남 (추상화한 메서드(함수)의 내부에는 내용을 넣을 수 없음)

        get;

        set;
        // get; set; 은 가능하지만, return 등을 써서 추가적으로 내용을 쓰는것 역시 불가능하다.
    }
}

 

이렇게 추상화한 클래스는 안에 추상화할 메서드(혹은 일반 함수(메서드)를 넣을 수도 있다.)를 넣을 수 있는데, 그렇게 되면 다른 클래스에서 이 메서드를 마치 설계도처럼 가져와 오버 라이딩해서 쓸 수 있다.

다만, 설계도이기 때문에 내용을 직접 추상화 메서드에 넣는 것은 불가능하다.

 

public class UseAbstract : Abstract
{
    public override double SetDamage()
    {
        // 오버라이딩할 내용들 
    }
}

 

사용할 경우에는 사용할 클래스에서 추상화 클래스를 상속을 한 뒤에 오버 라이딩으로 내용을 넣어 사용한다. 

그렇다면, 다른 클래스를 상속하면서 추상화 클래스의 메서드를 사용하려면 둘 중에 하나만 선택해서 상속해야 할까?

답은 그렇다 이다. 그래서 이런 경우에는 인터페이스 클래스를 사용할 수도 있다.

 

추상화 클래스
  - 다중 상속 불가
  - 클래스 특화적인 기능 설계

인터페이스 클래스
  - 다중 상속 가능
  - 다른 분류의 클래스인데 같은 기능을 넣고 싶을 경우 사용

 

인터페이스의 경우는 다시 위의 죽음의 다이아몬드 상황이 나올 수 있지 않느냐는 생각이 들 수 있다. (나 역시 그랬다.)

하지만, 기본적으로 그냥 Attack()을 써서 사용하면 기본적으로 상속하는 다른 클래스의 Attack()을 사용하게 된다.

인터페이스 클래스의 Attack()을 쓰고 싶을 경우는 해당 인터페이스를 인스턴스화 하여 변수에 저장하고, 변수.메서드명() 으로 실행하면 해당 인터페이스의 메서드를 실행할 수 있다.

 

 

제너릭 클래스

 

제너릭 클래스는 어떤 자료형이 들어오는지를 미리 정해두지 않고, 여러 자료형을 받을 때 유용하게 쓰인다.

 

public class GenericClass<T> where T : MonoBehaviour
{
    public void Doing(T value)
    {
        Debug.Log(value);
    }
}

 

T는 매개변수를 뜻하고, ClassName<T>까지만 쓰면, 어떠한 매개변수든 모두 받아서 실행하게 된다. 

where T : AnotherClass는 자료형에 조건을 다는 것으로, AnotherClass라는 클래스를 상속받는 자료형만 가져오게 된다.

 

ref / out

어떤 메서드에 매개변수로 값형 자료형을 넣을 때에 참조형처럼 사용되도록 하기 위해 매개변수에 붙이는 한정자이다.

 

둘의 차이가 있다면, ref는 미리 초기화해야 하고, out은 미리 초기화할 필요 없이 기존 값이 무시가 된다.

 

 

인덱서

클래스의 멤버들을 인덱싱으로 받을 수 있게 해준다.

 

private float[] temp;
  
public float this[int index]
{
    get { return temp[index];}
    set { temp[index] = value; }
}

 

위의 코드에서는 temp라는 값을 Result로 접근이 가능하다. 

 

Result r = new Result();
r[0] = 1f;

// r.temp[0] = 1f; 와 같은데, 인덱스를 사용하면 보는데 이해가 더 어려워짐

 

하지만, 한눈에 알아보기가 더 어려워져서 웬만한 경우에는 사용을 권하지 않는다.

 

 

구조체(Struct)와 클래스(Class)

 

구조체와 클래스는 거의 같은 느낌으로 쓰인다. (기능도 비슷하고)

심지어 메서드도 둘 다 넣을 수 있는데, 도대체 왜 나눠서 쓰는 것일까?

 

구조체(Struct)
  - 값형 자료구조
  - 상속이 불가능

클래스(Class)
  - 참조형 자료구조 (힙 메모리에 저장)
  - 상속이 가능

 

일단, 가장 큰 차이는 서로 저장되는 메모리 위치가 다르다는 것인데, 그 이유 때문에 필드(변수, 메서드 등등 내용)의 사이즈가 16Byte 미만일 때에는 Struct를 쓰는 것이 효율적이다. (물론 권장일 뿐, 게임을 만드는데 이보다 더 큰 구조체를 쓰는 경우가 엄청 많다고 한다.)

 

반면, 클래스를 넣거나 필드의 사이즈가 커질 경우에는 클래스를 쓰는 편이 좋다. 

 

 

네임스페이스

우리가 using을 통해서 라이브러리(외부 패키지)를 가져올 때에 using NameSpaceName으로 불러온다. 

그렇다면, 네임스페이스가 무엇일까?

 

간단하게 말해서 클래스들을 분류하는 더 큰 하나의 박스라고 생각하면 된다. 

클래스를 엄청 많이 생성하게 되면 그 이름들을 하나하나 다 다르게 하기가 쉽지 않다.

예를 들면, 내가 작성한 클래스 외에 유니티가 기본적으로 만들어 놓은 클래스를 쓰게 되는데, 만약 네임스페이스로 구분이 되어 있지 않으면, 우리는 어마 무시하게 많은 클래스들의 이름들을 피해 계속 새로운 클래스명을 정의해야 하는 번거로움이 생길 것이다.

하지만, 네임스페이스로 분류해 놓으면 같은 이름이라도 다른 네임스페이스 소속이면 아무런 문제가 되지 않는다.

그리고 우리는 이런 네임스페이스를 통해 패키지를 가져올 수도 있는 것이다.

 

using을 통해 패키지를 가져왔다면, 우리는 네임스페이스.클래스를 통해 인스턴스 하거나 메서드를 사용할 수 있다.

(참고로 네임스페이스 안에 네임스페이스를 넣을 수도 있다.)

 

 

델리게이트

우리가 함수 안에서 가끔 다른 함수를 호출하거나 하는 경우가 생긴다.

아니면, 같은 매개변수를 가지고 둘 이상의 함수를 호출하는 일도 생기는데, 이때마다 Func();을 써넣기 역시 아주 귀찮을 수 있다.

 

이럴 경우에 델리게이트를 만들어서 사용하면 편하다!

 

delegate float CustomDel(int num);	// 델리게이트 생성
// int를 입력받고, float를 출력하는 함수들을 담을 예정

CustomDel = Func01;		// 델리게이트에 Func01 하나만 넣기
CustomDel += Func02;	// 델리게이트에 Func02 추가
CustomDel += Func03;
CustomDel -= Func03;	// 델리게이트에서 Func03 제거 

CustomDel();			// 클래스들 실행 (보통 아래 코드가 더 권장됨)

CustomDel?.Invoke();	// 클래스가 아예 없는 경우(Null)가 아니면 함수들 실행

 

유니티 상에서는 이 델리게이트를 이용해서 onClick() 이벤트를 처리한다.

(실제로 해당 이벤트는 Action 클래스로 처리되는데, Action 클래스는 델리게이트이자 제너릭 클래스이다.)

 

이런 델리게이트 함수는 다른 상속받은 클래스의 메서드 안에 매개변수로 넣을 수도 있다.

 

void ProcessDelegate(CustomDel del)
{
    del?.Invoke();
}

 

델리게이트는 보통 이벤트같이 특정한 상황에 발생하는 함수들에 많이 쓰인다.

  - 델리게이트에 해당 이벤트 발생 시에 발생하는 클래스들을 담아두었다가 이벤트 발생 시에 델리게이트를 실행하는 방식 

 

 

람다식 

 

한 줄의 코드를 실행하는데 따로 함수를 만들어 사용하기 귀찮은 경우에 쓰는 유용한 식이다. 다만, 일회용으로 사용하는 경우만 한해서 이다.

 

사용하는 방식은 이렇다.

 

void Add(int a)
{
   a ++ ;
}

1. 이름 날려! (Add)
2. 델리게이트에 추가하는 경우, return 역시 따로 형을 선언하지 않음 (void)
  - 델리게이트에 이미 리턴 형이 다 정해져 있음(선언 시)
      ↘︎ 델리게이트에 넣는 경우 :  deligate1 = () => { 함수 내용; } ;   
      ↘︎ 델리게이트가 아닌 경우 :  (input) => { 함수 내용; return output } ;
2-2. 미리 델리게이트에서 input이 정해진 경우는 input도 자료형 안 쓰고 그냥 변수명을 쓰면 된다.
     (미리 int a = 0 등으로 선언할 필요 없음 → a)

 

CustomDel2 = (a) => { print($"{a}") };

// AddListener에서 사용하는 경우
this.GetComponent<Button>().onClick.AddListener(() =>
{
    SayOne();
    SayTwo();
});

 

이렇게 생성된 람다 함수는 따로 이름이 존재하지 않기에 반복 사용할 수 없다.

 

 

예외처리

Exeption(예외)의 상황에서 따로 처리하기 위한 것으로, try - catch ( - finally)를 통해서 처리한다.

 

보통 두 가지의 상황에서 사용하게 된다.

1. 의도하지 않은 경우 - 오류가 날 것이라고 생각 못했는데 나는 경우
2. 의도한 경우 - 오류가 있음을 직감하고, 이를 미연에 방지하고자 하는 경우 (핵 방지 시스템 등)

 

사용 방법은 이러하다.

void DivideWithZero()
{
   int num1 = 3;
   int num2 = 0;
   int result = 0;
 
   try // 일단 시도해봐 (에러 발생할지 보고 있을게)
   {
       result = num1 / num2; // 런타임 에러가 발생하면 catch로 
   }
   catch (DivideByZeroException e) // 해당 Exception이면  
   {
       Debug.Log($"Exception caught: {e.Message.ToString()}");
       // 이거 실행
   }
   finally // 최후에 출력하는 부분  
   {
       Debug.Log($"Result: {result}");
   }
}

 

참고로 C#에서 System 라이브러리에 기본으로 등록되어 있는 오류는 다음과 같다.

System.IO.IOException // 파일 입출력 → 파일이 없어! FileNotExist 등
// 자바/코틀린(안드로이드), 오브젝트C/스위프트(ios)에서는 명칭이 다를 수 있음

System.IndexOutOfRangeException 
// 배열의 인덱스를 넘어서는 것을 인덱싱하거나 하는 경우

System.ArrayTypeMismatchException // 파일이 안 맞을 경우

System.NullReferenceException // 어떤 객체를 참조했는데 Null인 경우
// 엄청 많이 남

System.DivideByZeroException // 특정 수를 0으로 나눴을 때

System.InvalidCastException // 특정 캐스팅(형변환)을 하는데
// 보통 에디터에서 뜨지만, 형을 정의하지 않는 경우에서 뜨는 경우가 있음

System.OutOfMemoryException // 메모리가 부족할 때 
// → 많이 뜨는데 잡기가 어려움 (이유를 알기가 쉽지가 않아서..)

/*
참고 <메모리 낭비 예시> - 이미지 포맷

RGBA : 8bit x 4(R, G, B, A) → FF FF FF (각각 8비트 → 32비트)
> RGB + 1Bit : 이미지 압축방식 중 하나로 RGB는 8비트로 하는데 알파값은 1비트로 사용한다는 뜻
예를 들면 서서히 투명해지는 알파가 아닌 딱 떨어지는 투명색이 필요한 경우, 알파에 8비트나 필요 없음 

32비트의 개념이 픽셀 하나에 들어가면… 해상도가 1920 x 1080 x 32(bit)
word → 컴퓨터가 한번에 읽을 수 있는 메모리 32비트, 64비트 등으로 이미지는 2의n승 만큼 컴퓨터가 읽어 오는데,
애매하게 512(2의 9승)보다 작은 500 X 500이면 컴퓨터가 알아서 남은 영역을 패딩을 해버린다. 
그렇게 되면 그 빈 패딩만큼 프로그램은 메모리 낭비를 하게 된다.
*/

System.StackOverflowException // 힘, 스택(메모리)에서 문제 생겼을 때

 

 

리플렉션 (Reflection)

런타임 중에 다른 프로그램(어셈블리) 혹은 라이브러리, 플러그인 등의 특정 클래스에 접근해야 하는 경우 사용한다.

(특히 내 어셈블리가 아닌 경우)

 

사실 내가 게임이나 메타버스를 만들면서 당장 많이 쓸 기능은 아니라 자세히는 정리하지 못했다.

 

 

 


 

Starters 부트캠프 4주 자습 내용 -  장애물 극복 게임, RPG 게임 만들기

 

요번 주에 내가 들은 강의는 총 두 개였다. 

 

1. C#Unity로 3D 게임 개발하기  [Udemy]

 

Video Game Development Using Unity: Code Games with C#

C# 기초부터 Unity 엔진 활용까지, 실제 게임 개발 프로젝트를 진행해보며 학습하는 3D 게임 개발의 모든 것

www.udemy.com

 

2. Unity 유니티 게임 만들기 RPG 게임 만들기  [Tistory]

 

unity 유니티 게임 만들기 RPG 게임만들기 1 Terrain 만들기

unity project를 생성하고 Scenes 폴더를 생성 한다음 Scene 이름을 RPGGame 을 하고 저장 합니다 GameObject -> Terrain 을 생성 합니다 Terrain 생성한 모습 그리고 Window -> Asset Store 를 열고 Standard..

magatron.tistory.com

 

그중에서도 나는 위에 유데미 강의를 추천한다.

C#이나 유니티에 처음인 사람들이 어려울 부분까지 자세히 그리고, 재밌게 알려주시기 때문이다. 

 

아래에는 위 두 강의를 들으면서 스스로 정리해둔 개념들을 정리해보았다.

참고로, 유데미 강의는 '섹션 2: 장애물 코스'까지 다 들은 상태이고, 아래 강의는 RPG 게임 만들기 전부 들었다.

 

Time.DeltaTime과 Time.time

게임을 컴퓨터의 프레임 레이트에서 독립시켜서 모두의 컴퓨터에서 오브젝트의 이동 거리 등을 통일하기 위해 쓰인다.

쉽게 말하면, 성능 좋은 컴퓨터나 나쁜 컴퓨터나 모두 초당 같은 거리를 이동하게끔 보정해준다는 것이다.

 

예를 들어 초당 프레임(FPS)이 10이라는 것은 1초당 10 프레임을 실행한다는 의미이다. 

즉, 한 프레임 당 걸리는 속도는 0.1초라는 것이고, 이 속도와 Time.deltaTime을 이용해 이런 식으로 이동거리를 보정합니다.

 

초당 프레임 X 프레임 당 속도 X (원하는) 초당 이동 거리

 

 

직렬화

어떠한 변수를 만들어 사용하는데, 유니티 에디터 상에서 조작하고 싶은 경우가 생깁니다.

혹은 Json 파일 등에서 정보를 가져오는 경우 직렬화가 필요합니다.

 

[SerializeField]
private float moveSpeed = 0.8f;

 

이런 식으로 직렬화를 하면 다른 클래스의 접근을 허용하지 않으면서도(private) 유니티 에디터 상에서는 접근 가능한 상태가 됩니다.

 

 

시네머신 (Cinemachine) 

물체의 이동에 따라서 카메라가 자동적으로 같이 이동하도록 만들고 싶을 때 등 씬에 있는 여러 카메라를 관리하고 쉽게 카메라에 법칙을 추가할 때 사용하는 패키지이다.

특히나 '타임라인'이라는 패키지와 함께 사용하면, 게임 시네마틱이나 중간 영상 같은데에서 카메라가 이동하며 찍는 장면도 만들 수도 있다.

 

시네머신을 이용해서 플레이어를 따라오는 카메라를 본 강의에서는 구현했는데 다음과 같이 하면 된다.

 

1. 패키지 매니저에서 시네머신 설치

패키지 메니저에서 시네머신 인스톨

 

2. 가상 카메라 생성하기

가상 카메라 생성하기

 

3. 가상 카메라 Z 축을 플레이어 위치와 맞추기

가상 카메라 위치 맞추기

 

4. CinemachineVirtualCamera 컴포넌트 Body값을 Framing Transposer로 변경

Body 설정

 

5. CinemachineVirtualCamera 컴포넌트 Follow에 플레이어 오브젝트 등록

Follow 오브젝트 설정

 

그 외에도 시네머신에 다양한 기능들이 존재하는데, 다른 기능들은 다음에 더 공부해보도록 하겠다.

 

 

MeshRenderer와 Collider, RigidBody

Renderer 컴포넌트는 오브젝트의 외피를 설정하는 컴포넌트로 active를 해제하면 오브젝트가 눈에 보이지 않게 된다.

Collider 컴포넌트는 오브젝트의 충돌 범위를 지정할 수 있는 컴포넌트이다.

RigidBody 컴포넌트는 개체들끼리 서로 부딪히는 충돌 판정을 부여하고, 개체에 질량 값이나 다른 물리력을 부여하기도 한다.

 

 

콜백 함수

C#에서 콜백 함수는 어떠한 이벤트가 발생했을 때에 저절로 실행되는 함수를 일컫는다.

(보통의 경우에 콜백함수는 함수 내 매개변수로 등록되어서 어떤 조건이 만족되었을 때에 실행되는 함수를 의미한다.)

 

유니티에서 Start(), Update(), Awake() 등의 함수 역시 콜백 함수라고 할 수 있다.

 

 

로컬 좌표계와 월드 좌표계

로컬 좌표계는 개인 오브젝트의 상대적인 좌표이다.

예를 들면 어떤 오브젝트가 부모 오브젝트에 속해있다고 했을 때, 해당 오브젝트는 부모 오브젝트를 기준으로 로컬 좌표를 잡는다.

 

만약 부모 오브젝트의 월드(절대) 좌표가 (10, 0, 0)이고, 해당 오브젝트의 로컬(상대) 좌표가 (5, 0, 0)이면, 해당 오브젝트의 월드(절대) 좌표는 (15, 0, 0)이다.

 

반면, 월드 좌표는 게임 전체적인 좌표로 고정적이고, 절대적인 좌표이다.

 

게임에서 이동이나 회전 등의 기능을 구현할 때에 상황에 따라 로컬 좌표를 변경하거나, 월드 좌표계를 변경하게 되므로 잘 배워야 하는 개념이다.

 

 

물체 이동과 회전 

이동하는데 키보드로 이동할 수도 있지만, ios 장르의 게임 경우에는 마우스 클릭으로 이동한다.

그렇다면, 클릭한 지점을 절대좌표로 받아서 이동하게 될 텐데.. 현재 좌표는 어떤 좌표로 받아야 할까?

당연히 절대 좌표로 받아서 이동시켜야 할 것이다.

 

이런 식으로 플레이어 등의 개체를 이동, 회전시키는 데에 위의 좌표계는 중요한 기준이다.

그렇다면 어떤 코드가 로컬 좌표계로 이동시키는 코드이고, 어떤 코드가 월드 좌표로 이동시키는 코드일까?

 

transform.Translate(moveX, 0, moveZ);			// 로컬 좌표로 이동
transform.position += new Vector3(moveX, 0, moveZ);	// 월드 좌표로 이동

 

transform.Translate() 메서드는 어느 방향으로 얼마나 물체를 이동시킬지 정하는 코드이고, 로컬 좌표 기준으로 방향을 잡아 움직인다.

transform.position을 직접 더하는 경우는 좀 다르게 월드 좌표를 기준으로 벡터 값을 더하거나 빼게 된다.

 

참고로 여기서 벡터값을 다른 식으로 주면 부드럽게 이동하는 등의 효과를 줄 수도 있다.

 

transform.position = Vector3.MoveTowards(transform.position, targetVector3, Velocity);
// 목적지를 정해 그곳까지 이동시키는 코드

transform.position = Vector3.SmoothDamp(transform.position, targetVector3, ref Velo, Velocity);
// 목적지를 정해 이동시키되 더 부드럽게 이동시키는 코드

 

가끔 이동을 위해 RigidBody에 물리 값을 주는 경우도 있는데 이 경우는 AddForce와 AddRelativeForce를 사용한다.

 

RigidBody rb;

// 로컬 좌표로 이동
rb.AddForce(Vector3.forward);

// 월드 좌표로 이동
rb.AddRelativeForce(Vector3.forward);

 

그렇다면, 회전의 경우에는 어떤 코드를 사용할까?

 

// 로컬 좌표 회전
transform.rotate(0, 10 ,0);

// 월드 좌표 회전
trnasform.rotation = Quaternion.Euler(10f, 20f, 30f);
transform.rotation = Quaternion.RotateTowards(transform.rotation, Quaternion.LookRotation(direction), maxDegreesDelta);
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(direction)

 

참고로 Quaternion과 Euler는 회전 값을 의미하며 우리가 흔히 얘기하는 "~도"의 값은 오일러(Euler)로 x, y, z축의 값을 가진다.

반면, Quaternion은 w 축을 하나 더 가지고 있어서 기존에 오일러에서 발생할 수 있는 짐볼락 현상을 없앨 수 있다는 장점이 있다.

 

참고로 transform.rotation의 경우에는 쿼터니움(Quaternion) 값을 가져서 기존 Vector3나 오일러 값을 대입할 수 없고, 모두 쿼터니움으로 환산해서 넣어야 한다.

 

 

RigidBody - is Kinematic

RigidBody는 유지해서 스크립트에서 물리력은 주고 싶은데, 기본적인 중력 등의 물리 값을 없애고 싶다면, RigidBody 컴포넌트의 is Kinematic을 체크해주면 된다. 

 

중력도 없어지면서, 회전과 이동 등의 물리력으로 인해 갑자기 플레이어가 넘어지거나 하는 것을 방지할 수 있다.

 

다만, 후에 다시 중력을 부여하고 싶다면 스크립트를 통해 새로 부여해야 한다.

 

 

Collider - is Trigger

충돌 판정을 받게끔 하지만, 충돌은 안 나도록 해준다.

 

 

디자인 패턴 중 '싱글톤'

컨버터나 입력값, 사운드, json 등의 유틸리티를 관리하는 클래스는 '싱글톤'이라는 디자인 패턴을 사용하는 것이 권고된다.

 

'싱글톤' 기법이란 그냥 해당 클래스를 정적 클래스로 선언해서 사용하는 것이다.

사용 방법은 다음과 같다.

 

public class InputManager
{
    public static InputManager instance;	// 정적멤버 선언
    
    private void Awake() 			// null인 경우 해당 클래스를 instance에 저장
    {
        if (instance == null)
        {
            instance = this;
        }
    }
    
    public void State()
    {
    
    }
}

public class Player
{
	InputManager.instance.State();		// 정적 클래스의 메서드 사용 
}

 

아래 배울 프로퍼티를 사용하면, 정보를 가져올 수만 있게끔 할 수도 있다.

 

pulic static ClassName Instance
{
	get
	{
		if (instance == null)
		{
			return null;
		}
	return instance;
	}
}

 

 

프로퍼티 (Property)

프로퍼티를 사용하면 어떤 필드를 다른 클래스에서 가져오는데 '반환'과 '할당' 중에 어떠한 것만 허락하거나, 미리 정의한 내용대로 반환, 할당시킬 수 있다.

 

class ClassName
{
	int fieldName;

	public int PropertyName(보통은 FieldName)
	{
		get
		{
			return fieldName;
		}
		set
		{
			fieldName = value;
		}
	}
	
}

 

만약 내용이 크게 없고 get; set; 만 필요하다면 다음과 같이 간단하게 적으면 된다.

 

public int age { get; set; };

 

 

오브젝트 풀링 기법 (Object Pulling)

오브젝트 풀링의 경우 역시 위에 싱글톤과 같이 유용한 디자인 패턴 중 하나이다.

특히나 엄청 많이 개체를 생성하고 재활용해야 하는 경우 (주로 게임에서) 사용하는 디자인 패턴으로 메모리 할당과 해제에 들어가는 메모리 파편화를 방지하기 위해 쓰인다.

 

1. 먼저 오브젝트 풀에 미리 원하는 만큼의 최대 개수의 오브젝트를 만들어두고 (하위 오브젝트로) 비활성화시켜둔다.

2. 상황이 되어 오브젝트를 호출하는 경우 만들어둔 풀에서 개체를 하나씩 빼서 활성화시키고 사용한다.

3. 만약 모든 오브젝트가 사용 중이라 더 빼서 쓸 수 있는 오브젝트가 없다면, 새로운 오브젝트를 인스턴스화 하고, 풀에 추가한 뒤 사용한다.

4. 사용한 오브젝트는 다시 비활성화해서 풀에 넣는다.

 

3번의 경우 때문에 보통 오브젝트 풀링은 동적으로 크기 조절이 가능한 자료구조로 구현하는 것이 바람직하다. 

일단 나는 큐로 구현하기로 하였다. (구현하는 중이다.)

 

(아래 블로그를 참고하면, 더 자세한 설명을 볼 수 있고, 나 역시 이 블로그를 참고해서 배웠다.)

 

[Unity3D] Programming - 오브젝트 풀링 기법 구현하기

Programming - 오브젝트 풀링 기법 작성 기준 버전 :: 2019.2 프로그래밍에서 오브젝트를 생성하거나 파괴하는 작업은 꽤나 무거운 작업으로 분류된다. 오브젝트 생성은 메모리를 새로 할당하고 리소

wergia.tistory.com

 

CharacterController 컴포넌트

충돌에 의한 물리적인 값을 부여할 때에 RigidBody 대신에 사용할 수 있는 컴포넌트이다.

캐릭터 컨트롤러는 힘(관성, 중력 등)에 의해 영향을 받지 않고, Move 함수가 호출되었을 때에만 움직이게 하고, 충돌에 의해 힘이 가해졌을 때에만 움직임을 수행한다.

 

(아래 블로그를 참고하면, 더 자세하게 이해가 될 겁니다.)

 

유니티)캐릭터 컨트롤러 (Character Controller)

Character Controller 캐릭터 컨트롤러(컴포넌트) 캐릭터 컨트롤러(CharacterController)는 리지드바디(rigidbody)를 다루지 않고, 충돌에 의한 움직임을 다루기 쉽도록 해준다. 캐릭터 컨트롤러는 힘(관성, 중

funfunhanblog.tistory.com

 

 

코루틴 (Coroutine)

어떠한 동작 등을 지속적으로 구현하기는 하는데, 중간중간 대기 시간이 있다면, Update()에서 구현하는 것이 옳을까?

 

예를 들어 5초의 쿨타임을 가지고 포션을 먹는 행동을 계속 반복하는 기능을 업데이트를 통해 구현하게 된다면...

60 fps 기준) 쿨타임 시간에도 60(번) X 5 = 120번 의미 없이 코드를 실행하게 된다.

 

반면 코루틴은 대기 시간 동안 코드의 제어를 다른 곳에 부여하다가 대기 시간이 끝나면, 중단했던 코드의 아랫부분을 마저 실행시킨다.

 

IEnumerator 코루틴명() 
{
	yield return new WaitForSeconds (5.0f);
	isDelay = false;
}

 

여기서 중요한 것은 대기 시간을 표시할 때에 yield return을 쓴다는 것이다.

 

yield return 반환 시간 반환
  - 시간까지 유니티에 제어권을 돌려주어서 아래의 코드를 실행하지 않다가 반환 시간이 되면 다음 코드를 실행하는 것
  - null이면 1 프레임만큼 코드 동작을 중지한다.

new WaitForSeconds (5.0f)
  - 5초 동안 중지

yield break 반환 시간 반환
  - 시간까지 기다리는 것은 같으나 반환 시간이 되면 바로 코루틴을 끝낸다.

 

이렇게 만든 코루틴을 실행시키려면 'StartCoroutine(코루틴명())'을 작성하면 된다.

 

// 코루틴 실행
StartCoroutine(Corutine1());
StartCoroutine("Corutine1");

// 코루틴 정지
StopCoroutine("Corutine1");	// 해당하는 코루틴 정지
StopAllCoroutines(); // 모든 코루틴 정지

 

 


 

Starters 부트캠프 4주차 후기

이번 주는 개발 공부 1주차 때와 마찬가지로 정말 핵심적인 C# 프로그래밍의 개념들을 익히면서 그동안 모르고 따라 쓴 코드들의 원리를 조금이나마 이해할 수 있게 된 것에 매우 기뻤다.

특히, 자습으로 유데미, 기타 강의의 프로젝트를 같이 진행하면서 배웠던 개념들이 등장하면, 무언가 반갑고, 자신감이 더 늘어났던 것 같다.

 

현재 유데미 등의 강의로 두 개의 프로젝트를 따라서 완성했는데, 다음 주부터는 그 기능까지 혼자서도 만들어보는 경험을 조금 가져보려고 하고 있다.

 

아래는 따라서 완성한 내 프로젝트 모습이다.

 

유데미 장애물 코스 게임 결과물

 

블로그 RPG 강의 결과물

 

 

유데미 코리아 바로가기 : 

 

Udemy Korea - 실용적인 온라인 강의, 글로벌 전문가에게 배워보세요. | Udemy Korea

유데미코리아 AI, 파이썬, 리엑트, 자바, 노션, 디자인, UI, UIX, 기획 등 전문가의 온라인 강의를 제공하고 있습니다.

www.udemykorea.com

 

💡 본 포스팅은 유데미-웅진씽크빅 취업 부트캠프 유니티 1기 과정 후기로 작성되었습니다.