
유니티에서 C# 객체지향 프로그래밍(OOP)은 복잡한 게임 시스템을 관리하고 유지보수성을 높이는 데 필수적이다. 객체 지향 프로그래밍을 이용하면 클래스와 상속, 다형성 등을 통해 코드를 모듈화하여 재사용성을 극대화하고, 확장성을 높여 새로운 기능을 쉽게 추가할 수 있다. 또한, 코드의 구조를 명확히 나눔으로써 협업 시에도 개발자 간의 작업 충돌을 최소화하고, 수정할 때 특정 부분만 수정하면 되므로 오류 발생 가능성을 줄일 수 있다. 이를 통해 효율적인 개발과 코드 관리가 가능해진다. 유니티 C#에서는 네임 스페이스, 클래스, 구조체, 제네릭 타입 등을 통해 이를 구현할 수있다.
코드를 간소화하려면 반복되는 부분을 별도의 메서드로 추출해 공통적인 기능을 모듈화하는 것이 좋습니다. 예를 들어, 각 손의 진행 상태를 확인하는 로직을 한 메서드로 통합하고, 왼손과 오른손에 대해 동일한 메서드를 호출하면 됩니다. 아래는 코드 간소화 예시입니다:
- 복적인 Get 메서드는 => 구문으로 간결하게 작성하여 코드 길이를 줄였습니다.
- Hand<T> 클래스에서 Update 메서드들은 가독성을 위해 한 줄로 압축했습니다.
- 코드가 Awake()에서 한 번만 실행되는지 확인하여 instance 관련 조건 검사를 정리했습니다
https://devhycho1107.tistory.com/42
C#의 델리게이트(delegate)
1. 네임스페이스 선언
네임스페이스 선언은 클래스 앞에 "namesapce 네임스페이스명" 선언 후 {}로 묶으면 됩니다.
사용 방법 : 네임스페이스 |
namespace 네임스페이스-이름 { class A { } class B { } .... } |
네임스페이스는 중첩을 허용합니다.
중첩 방법은 첫번째는 네임스페이스를 중첩 선언하는 방법입니다.
네임스페이스 중첩1 |
namesapce 네임스페이스-이름-1 { namesapce 네임스페이스-이름-2 { class A { } class B { } .... } } |
두 번째 방법은 . 키워드를 사용하는 방법입니다.
네임스페이스 중첩2 |
namesapce 네임스페이스-이름-1.네임스프이스-이름-2 { class A { } class B { } .... } |
2. 네임스페이스 참조
네임스페이스를 사용하기 위해서는 다음 2 가지 방식으로 사용됩니다.
2. 1 전체 네임스페이스를 다 적어주는 방식.
예제 : 전체 네임스페이스를 다 적어주는 방식 |
UnityEngine.Debug.Log("네임스페이스 1"); |
2. 2 using 지시문을 사용.
using 지시문은 선언문에 using 지시문을 선언해줍니다.
using 지시문 |
using UnityEngine; public class NamespacesExample : MonoBehaviour { void Start() { Debug.Log("네임스페이스 1"); } } |
3. 예제
네임스페이스 선언
using UnityEngine;
namespace Namespace1
{
public class NamespaceDefinitionExample
{
public static void DebugLog(string message)
{
Debug.Log(message);
}
}
}
네임스페이스 사용
using UnityEngine;
using Namespace1;
public class NamespaceUsageExample : MonoBehaviour
{
void Start()
{
NamespaceDefinitionExample.DebugLog("네임스페이스");
}
}
4. using 문
C#에서 using 키워드는 using 지시문, using 별칭 지시문, using문으로 사용됩니다.
4.1 using 지시문
지시문에 using 지시문은 네임스페이스 형식 사용 허용에서 사용됩니다.
예제 : using 지시문, 네임스페이스 |
using UnityEngine; |
4.2 using 별명 지시문
네임스페이스 또는 형식의 별명을 만드는 데 사용됩니다.
using 지시문을 사용하다 보면, 메서드명이 겹치는 경우가 발생합니다.
이때 using 별명 지시문을 사용하여, 어떤 메서드를 사용할지를 정해 줄 수 있습니다.
아래 예는 Debug라는 메서드가 겹쳐서 using 별명 지시문을 사용합니다.
using UnityEngine;
using System.Diagnostics;
using Debug = UnityEngine.Debug;
public class UsingAliasDirectiveExample : MonoBehaviour
{
void Start()
{
Debug.Log("UsingAliasDirective");
}
}
하지만, 지시문에 using 별명 지시문을 선언해주면, Debug는 UnityEngine.Debug가 사용됩니다.
4.3 using 문
using 문은 객체의 범위를 정의할 때 사용됩니다.
이렇게 정의된 객체는 using 문 블록을 벗어나게 되면 자동적으로 Dispose 됩니다.
Dispose는 메모리를 반납하는 메서드로 리소드를 정리해 줍니다.
File이나 Database, www 등 관리하기 힘든 리소스들의 관리를 해 줍니다.
using System.Collections;
using UnityEngine;
public class UsingStatementExample : MonoBehaviour
{
public string url = "http://images.earthcam.com/ec_metros/ourcams/fridays.jpg";
IEnumerator Start()
{
using (WWW www = new WWW(url))
{
yield return www;
Renderer renderer = GetComponent<Renderer >();
renderer.material.mainTexture = www.texture;
}
}

1. 클래스 개요
객체 지향 프로그래밍(OOP)은 객체(Object)를 사용하여 프로그램을 구성하는 방법으로, 객체는 변수(Variable)와 메서드(Method)로 이루어진 기능 실행 단위이다. 클래스(Class)는 이러한 객체를 생성하기 위해 변수와 메서드를 정의한 사용자 정의 데이터형(User Defined Data Type)이며, 클래스에 의해 생성된 객체는 인스턴스(Instance)로 불린다. 클래스는 논리 단위에 속하는 데이터와 동작 집합을 캡슐화하는 데이터 구조로, 데이터 및 동작은 메서드, 속성, 이벤트 등으로 구성된 멤버로 이루어져 있다.
1.1 클래스 선언
클래스는 접근제한자 class키워드 클래스 이름으로 선언한다.
public class 클래스-이름
{
// 필드, 속성, 메서드, 이벤트
}
1.2 접근 제한자(Access Modifier)
접근 제한자는 클래스 내에 멤버(필드, 메서드, 속성, 이벤트 등)의 접근을 제한하는 역할을 한다.
- public : 클래스의 외부에서도 접근 가능.
- protected : 상속받은 자식 클래스에게만 접근 가능.
- private : 클래스의 내부에서만 사용.

아무 선언을 하지 않으면, 기본 접근 제한자는 private이다.
1.3 멤버(Member)
클래스는 데이터와 동작을 나타내는 멤버가 있다.
멤버 | 내용 | 예제 |
생성자 | 클래스를 초기화할 때 실행됩니다. | Example(...) |
소멸자 | 클래스를 종료할 때 실행됩니다. | ~Example(...) |
상수 | 상수 | const float pi = 3.14f; |
필드 | 변수, public 메서드 | int field; |
메서드 | 메서드 | void Method(){...}; |
속성 | 클래스 밖에서는 변수처럼, 클래스 안에서는 메서드처럼 사용됩니다. | int Property {get; set;} |
이벤트 | 속성처럼 사용할 수 있는 메서드입니다. | event EvnetHandler Event; |
인덱서 | 속성처럼 사용할 수 있는 배열입니다. | int this[int i]{...} |
연산자 | 오버로드 된 연산자입니다 | static Example operator +(Exmaple x, Example y){...} |
클래스 | 클래스 내의 클래스입니다. | class Example2{...} |
1.4 객체 만들기(인스턴스화, Instantiation)
객체는 참조 형식이다. 클래스에서 new 키워드를 사용하여 선언하면 된다.
using UnityEngine;
public class Class1 // 클래스 선언
{
public int m_IntVariable;
private string m_StringVariable;
public void DebugLog(string message)
{
m_StringVariable = message;
Debug.Log(message);
}
}
public class ClassExample : MonoBehaviour
{
private Class1 m_Class1 = new Class1(); // 객체 인스턴스화
void Start()
{
m_Class1.DebugLog("Example"); // 출력 : Example
}
}
1.5 생성자(Constructor)
생성자는 인스턴스를 생성할 때 반드시 호출되는 초기화 메서드로, 클래스 이름과 동일한 이름을 가지며, 인수를 사용할 수 있지만 반환값은 없다. 기본 생성자는 인수가 없는 형태로, 특별한 동작 없이 인스턴스를 초기화하며, 필요에 따라 생략할 수 있습니다. 이로 인해 사용자는 클래스를 정의할 때 기본 생성자를 명시하지 않아도 기본적으로 제공되는 생성자를 통해 인스턴스를 생성할 수 있다. 만약, 기본 생성자가 아닌 인수를 가지는 생성자를 사용하게 되면, 기본생성자 생성이 억제된다.
using UnityEngine;
public class Player : MonoBehaviour
{
// 필드
private string playerName;
private int playerScore;
// 기본 생성자
public Player()
{
playerName = "Default Player"; // 기본값으로 초기화
playerScore = 0;
}
// 매개변수를 받는 생성자
public Player(string name, int score)
{
playerName = name; // 매개변수로 받은 값을 필드에 할당
playerScore = score;
}
// Start 메서드
void Start()
{
// 객체 생성 후 초기화 로직
Debug.Log($"Player Name: {playerName}, Player Score: {playerScore}");
}
}
1.6 정적(Static)
정적 멤버(static member)는 객체의 인스턴스 생성 없이 클래스 자체를 통해 직접 접근할 수 있는 멤버를 말한다. 이러한 정적 멤버는 클래스에 속하며, 클래스의 모든 인스턴스가 공유하는 특징을 가지고 있다. 정적 멤버를 선언할 때는 static 키워드를 사용하며, 이 키워드는 변수, 메서드, 또는 이너 클래스에 적용될 수 있다. 예를 들어, 정적 변수는 클래스에 대한 모든 인스턴스가 동일한 값을 참조하게 하여 메모리 사용을 효율적으로 관리할 수 있게 해준다. 정적 메서드는 특정 객체의 상태에 의존하지 않고 클래스 수준에서 공통적으로 사용되는 기능을 구현하는 데 유용하다.
using UnityEngine;
public class GameManager : MonoBehaviour
{
// 정적 변수: 모든 인스턴스가 공유하는 점수
public static int score = 0;
// 정적 메서드: 점수를 증가시키는 기능
public static void IncreaseScore(int amount)
{
score += amount;
Debug.Log("Score: " + score);
}
// 정적 메서드: 점수를 초기화하는 기능
public static void ResetScore()
{
score = 0;
Debug.Log("Score reset!");
}
}
정적 멤버 중 static이 없는 멤버를 인스턴스 멤버라고 하는데, 정적 멤버는 인스턴스 멤버로는 접근할 수 없다.
public class Class1
{
public static int staticInt;
public int instanceInt;
public static void StaticMethod()
{
staticInt++;
instanceInt++; // 에러
}
}
Class1.staticInt = 1;
하지만, 다른 멤버들은 호출할 때 메모리에 올라가게 되지만, 정적 멤버는 프로그램이 시작할 때부터 종료될 때까지 메모리에 올라가 있기 때문에 메모리 낭비가 생길 수 있다.
1.7 속성(Property, 프로퍼티)
속성(Property)은 객체 지향 프로그래밍에서 클래스의 데이터를 안전하게 관리하기 위한 기능으로, 클래스 외부에서는 변수처럼 사용되지만, 내부에서는 메서드처럼 동작하여 데이터 접근과 수정 시 추가적인 로직을 적용할 수 있다. 속성은 주로 캡슐화를 통해 데이터의 직접적인 접근을 제한하고, 데이터를 읽고 쓰는 과정에서 검증 또는 변경 작업을 수행하는 데 유용하다. 속성을 정의하려면 일반적으로 두 개의 메서드, 하나의 getter(값을 반환하는 메서드)와 하나의 setter(값을 설정하는 메서드)가 필요하며, 이를 통해 필드에 대한 읽기와 쓰기를 제어한다.
public class Player
{
private int health; // 필드
// Health 속성
public int Health
{
get { return health; } // 값을 반환하는 getter
set
{
if (value < 0) // 값 검증
health = 0; // 건강이 0 미만이면 0으로 설정
else
health = value; // 값을 설정
}
}
// 생성자
public Player(int initialHealth)
{
Health = initialHealth; // 초기 건강 설정
}
}
private int property;
public int Property
{
get { return this.property}
set { this.property = value}
}
get과 set 둘 다 아무 처리를 하지 않는다면 간단하게 작성할 수 있는데 이것을 자동 프로퍼티라고 한다.
public int Property{ get ; set; }
또한 속성의 get이나 set에 접근 제한자를 사용하여 외부에서 접근을 제한할 수 있다. 아래 예제는 외부에서 읽을 수 있지만, 수정을 할 수 없는 속성이다.
public int Property{ get ; private set; }
유니티에서는 속성은 public으로 선언하여도 Inspector(인스펙트) 창에 나타나지 않는다. 이때 [SerializedField]을 사용하면, Inspector 창에도 나타나게 된다.
[SerializedField]
private int property;
public int Property
{
get { return this.property}
set { this.property = value}
}
1.8 인덱서(Indexer)
인덱서(Indexer)는 클래스가 배열처럼 동작하도록 만들어 주는 기능으로, 객체 내 데이터를 배열의 인덱스처럼 [] 대괄호를 사용하여 접근할 수 있게 해준다. 클래스가 배열은 아니지만 인덱서를 사용하면 클래스 내부의 데이터나 필드를 마치 배열처럼 다룰 수 있으며, 속성과 유사하게 데이터를 읽고 쓸 때 get과 set을 통해 제어할 수 있다. 인덱서의 특징은 속성 이름 대신 this[] 키워드를 사용하고, 배열의 첨자처럼 인덱스를 전달한다는 점이다.
using UnityEngine;
public class Item
{
public string itemName;
public Item(string name)
{
itemName = name;
}
}
public class Inventory
{
private Item[] items = new Item[5]; // 아이템 배열
// 인덱서 정의
public Item this[int index]
{
get
{
if (index >= 0 && index < items.Length)
return items[index]; // 유효한 인덱스 범위에서 아이템 반환
else
Debug.LogError("Invalid index");
return null;
}
set
{
if (index >= 0 && index < items.Length)
items[index] = value; // 유효한 인덱스 범위에서 아이템 설정
else
Debug.LogError("Invalid index");
}
}
}
priavte int[] x;
public int this[int I]
{
get { return x[i]; }
set { x[i] = value; }
}
1.9 메서드(Method)
1.9.1 메서드
메서드란 클래스 내에서 코드 블록을 별도의 이름을 붙여서 다른 위치에서 호출하도록 만든 것입니다.
메서드는 일반적으로 한 가지 이상의 기능을 하기 위한 코드들의 집합으로 메서드 정의와 호출로 나눌 수 있습니다.
먼저 메서드 정의는 아래와 같습니다.
그리고, 이 메서드의 호출은 아래와 같습니다.
이 메서드는 매개변수와 변환 값을 지정하여 값을 주고받습니다.

매개변수란 메서드를 호출 할 때, 메서드 안에서 사용하는 변수를 괄호 안에 선언하는 것입니다.
이 매개변수의 개수는 제한이 없지만, 값은 메서드 이름의 매개변수의 개수와 같아야 합니다.
매개변수 개수가 틀리면, 다른 메서드로 인식하기 때문입니다.변환 값이란 이 메서드가 실행되면, 얻어지는 값을 의미합니다.일반적으로 메서드를 호출할 때와 메서드를 정의할 때의 데이터 형식은 같아야 합니다.그리고, 변환형 데이터 형식에는 void라는 데이터 형식이 있습니다.
void는 변환 값이 없이 메서드를 실행만 합니다.그리고 return이라는 키워드는 변환 값을 의미합니다.
이때, 변환 값은 변환형과 같은 데이터 형식이어야 합니다.
그리고 변환형이 void일 경우, return이 없어도 되고, return 만 적어두고 값을 없애도 됩니다.
using UnityEngine;
public class MethodeExample : MonoBehaviour
{
void Start()
{
int parameter1 = 1, parameter2 = 2;
int intMethod = IntMethod(parameter1, parameter2);
Debug.Log(intMethod); // 출력 : 3
float parameter3 = 1.2f;
VoidMethod(parameter3.ToString()); // 출력 : 1.2
}
int IntMethod(int p1, int p2)
{
return (p1 + p2);
}
void VoidMethod(string message)
{
Debug.Log(message);
return; // 생략 가능
}
}
1.9.2 매개변수
1.9.2.1 값 전달(Pass by Value)
C#은 메서드에 매개변수를 전달할 때 값을 복사해서 전달하는 값 전달(Pass by Value)입니다.
매서드 내에 전달 된 매개변수는 메서드가 끝나고 함수가 리턴되면, 매개변수 값은 원래 값으로 유지됩니다.
using UnityEngine;
public class PassByValueExample : MonoBehaviour
{
void Start()
{
int c = Add(1, 2);
Debug.Log(c); // 출력 : 3
}
int Add(int a, int b)
{
return a + b;
}
}
1.9.2.2 참조 전달(Pass by Reference)
메서드에 매개변수를 전달할 때, 참조로 전달할 수 있는데 이때 사용하는 키워드가 ref와 out입니다.
참조 전달되는 변수는 메서드 내에서 변경된 값은 리턴 후에도 유효합니다.
그런 이유로 ref와 out은 리턴 값처럼 사용할 수 있습니다.
ref | out | |
공통점 | 참조 전달 | 참조 전달 |
차이점 | 변수가 꼭 초기화하고 사용해야 함. | 변수 초기화를 꼭 할 필요는 없음. |
using UnityEngine;
public class PassByReferenceExample : MonoBehaviour
{
void Start()
{
int a = 1, b = 2;
int c = 0; // 초기화
bool b1 = RefMethod(a, b, ref c);
int d = 3, e = 4;
int f; // 초기화하지 않음
bool b2 = OutMethod(d, e, out f);
}
bool RefMethod(int a, int b, ref int c)
{
c = a + b;
return true;
}
bool OutMethod(int d, int e, out int f)
{
f = d + e;
return true;
}
}
1.9.3 네임드 매개변수(Named Parameter)
매 세드에 매개변수를 전달할 때 일반적으로 순서대로 매개변수가 넘겨집니다.
하지만, C# 4.0부터는 위치에 상관없이 매개변수 이름을 지정하면 매개변수를 전달할 수 있습니다.
사용방법은 매개변수 이름을 매개변수 앞에 선언해 주고, (:)클론 을 적어 둔 뒤 변숫값을 적어두면 됩니다.
using UnityEngine;
public class NamedParameterExample : MonoBehaviour
{
void Start()
{
NamedParameter(name: "Coderzero", height: 174.5f, age: 47);
}
void NamedParameter(string name, int age, float height)
{
Debug.LogFormat($"Name : {name} Age : {age} Height : {height}");
// 출력 : Name : Coderzero Age : 47 Height : 174.5
}
}
1.9.2.3 매개변수 기본값 (Optional Parameter)
C# 4.0 부터 매개변수에 기본값을 할당할 수 있습니다.
기본값을 할당받은 매개변수는 생략이 가능합니다.
이 매개변수를 Optional Parameter라고 하는데, 매개변수들 중 맨 마지막에 위치하여야 합니다.
그리고, Optional Parameter가 여러 개일 경우 Optional이 아닌 매개변수 뒤에 위치하여야 합니다.
사용방법은 메서드를 선언할 때 매개변수에 기본값을 선언해 주면 됩니다.
using UnityEngine;
public class OptionalParameterExample : MonoBehaviour
{
void Start()
{
OptionalParameter(1, 2.3f); // 출력 : 1
OptionalParameter(1, 2.3f, false); // 출력 : 2.3
}
void OptionalParameter(int i, float f, bool isInt = true)
{
if (isInt)
Debug.Log(i);
else
Debug.Log(f);
}
}
1.9.2.4 Params
메서드는 매개변수의 개수가 고정되어 있습니다.
하지만, 매개변수에 앞에 params 키워드를 사용하여 가변적인 배열을 매개변수로 만들 수 있습니다.
params는 한 메서드 안에 하나만 존재해야 하며, 맨 마지막에 위치해야 합니다.
사용방법 매개변수를 선언할 때 앞에 params 키워드를 적어 주고, 배열 형태로 선언하면 됩니다.
using UnityEngine;
public class ParamsExample : MonoBehaviour
{
void Start()
{
float f1 = Add(1, 2);
Debug.Log(f1); // 출력 : 3
float f2 = Add(3, 4, 5);
Debug.Log(f2); // 출력 : 12
}
float Add(params float[] fs)
{
float result = 0;
for (int i = 0; i < fs.Length; i++)
{
result += fs[i];
}
return result;
}
}
1.9.3 확장 메서드(Extension Method)
일반적으로 메서드는 클래스 안에 선언되어 사용됩니다.
하지만, 확장 메서드는 이미 만들어진 클래스에 메서드를 추가하여 사용할 수 있습니다.
선언 형식은 정적(static) 클래스를 먼저 정의하고 정적(static) 메서드를 선언하면 됩니다.
확장 메서드의 첫 번째 매개변수는 이미 만들어진 클래스 이름으로 형식을 지정하며, 매개변수 앞에 this 한정자가 넣습니다.
2번째 매개변수부터는 실제로 사용하는 매개변수를 넣으면 됩니다.
사용방법 : 확장 메서드 선언 |
public static class 클래스-이름 { public static 데이터-형식 메서드-이름(this 이미-만들어진-클래스-이름 식별자, 매개변수...) { ... // return [변환 값]; // 데이터 형식이 void가 아니면 변환 값이 생김 } } |
확장 메서드는 정적 메서드로 정의되지만, 인스턴스 메서드 구문을 사용하여 호출됩니다.
using UnityEngine;
public static class ExtensionMethod
{
public static string NewToLower(this string str) // 확장 메서드, string 클래스에 ToLowerMethod 메서드 추가
{
return string.Format("ToLower : {0}", str.ToLower());
}
}
public class ExtensionMethodExample : MonoBehaviour
{
void Start()
{
string s = "AbCde";
Debug.Log(s.NewToLower()); // 출력 : ToLower : abcde,
}
}
1.9.3 튜플(Tuple)
C# 7.0 이상부터 지원하는 기능입니다.
일반적으로 메서드는 하나의 값만을 변환받을 수 있습니다.
하지만 튜플을 사용하게 되면, 복수의 값을 리턴 받을 수 있습니다.
사용방법은 메서드 선언할 때 데이터 형식을 하나 넣는 대신, 괄호()를 사용하여 콤마를 사용하여 여러 가지 데이터 형식을 순차적으로 넣어 주고, 메서드 안에서 변환 값을 괄호()와 콤마를 사용하여 순차적으로 넣어주면 됩니다.
using System.Collections.Generic;
using UnityEngine;
public class TupleExample : MonoBehaviour
{
void Start()
{
List<int> list = new List<int> { 1, 2, 3, 4, 5 };
var r = Average(list);
Debug.LogFormat("Count : {0}, Average : {1}", r.count, r.average); // 출력 : Count : 5, Average : 3
}
(int count, float average) Average(List<int> data)
{
int cnt = data.Count;
int sum = 0;
for (int i = 0; i < cnt; i++)
{
sum += data[i];
}
float avrg = sum / cnt;
return (cnt, avrg);
}
}
2. 상속(Inherutabce)
하나의 클래스를 사용하다가, 그 클래스의 멤버를 그대로 사용하면서, 다른 기능을 사용하고 싶은 경우가 있습니다.
이때 사용하는 기법이 상속입니다.
상속은 이미 만들어진 클랙스의 기능을 바탕으로 새로운 클래스를 만들어서 새 멤버를 추가할 수 있습니다.
우리는 새로운 C# 스크립트를 만들 때마다, MonoBehaviour 상속을 받은 클래스가 생성됩니다.
using UnityEngine;
public class UnityInherutabceExample : MonoBehaviour
{
void Start()
{
}
}
여기서 클래스에서 상속을 시키는 클래스를 부모 클래스(Base Class)라고 하고, 파생시키는 클래스를 자식 클래스(Derived Class)라고 합니다.
자식 클래스를 만들 때 클래스 이름 뒤에 :(클론)을 붙이고 그 뒤에 부모 클래스 이름을 적으면 됩니다.
using UnityEngine;
public class BaseClass
{
public string m_StringVariable;
public int m_IntVariable { get; set; }
}
public class DerivedClass1 : BaseClass
{
public void Method()
{
Debug.Log(m_StringVariable);
}
}
public class DerivedClass2 : BaseClass
{
public void Method()
{
Debug.Log(m_IntVariable);
}
}
public class InheritanceExample : MonoBehaviour
{
DerivedClass1 m_BaseClass1 = new DerivedClass1();
DerivedClass2 m_BaseClass2 = new DerivedClass2();
void Start()
{
m_BaseClass1.m_StringVariable = "DerivedClass1";
m_BaseClass1.Method(); // 출력 : DerivedClass1
m_BaseClass2.m_IntVariable = 3;
m_BaseClass2.Method(); // 출력 : 3
}
}
3. 추상(Abstract)
3.1 추상 클래스
추상 클래스란 그 클래스 자체는 객체(인스턴스)를 생성하지 못하고, 자식 클래스로만 객체(인스턴스)를 생성할 수 있는 클래스입니다.
추상 클래스 생성 방법은 클래스 앞에 abstract 키워드를 붙이면 됩니다.
예제 : 추상 클래스 |
abstract public class BaseClass { } public class DerivedClass1 : BaseClass { } // BaseClass bc1 = new BaseClass(); // 에러 DerivedClass1 dc1 = new DerivedClass1(); // 정상 |
3.2 추상 메서드
추상 메서드는 abstract로 정의된 추상 클래스에서 정의된 메서드입니다.
추상 메서드는 메서드 앞에 abstract 키워드를 붙이면 됩니다.
추상 메서드는 메서드 이름의 정의가 있지만, 구현 없이 프로토타입(Prototype)만 가지고 있습니다.
자식 클래스에서는 반드시 구현을 해야 하는데, 이 것을 오브라이드(Override)라고 합니다.
그래서 자식 클래스에서 메서드 앞에 override 키워드를 붙어야 합니다.
예제 : 추상 메서드 |
abstract public class BaseClass { abstract public void Method(); } public class DerivedClass1 : BaseClass { override public void Method() { Debug.Log("Abstract Method"); } } |
4. 인터페이스(Interface)
일반적으로 클래스는 하나의 클래스가 하나의 클래스에 상속이 가능합니다. 다시 얘기하면 다중 상속이 불가능합니다.
하지만 인터페이스는 다중 상속이 가능합니다.
인터페이스는 추상 메서드만을 포함한 클래스라 할 수 있습니다.
인터페이스도 추상 메서드이기 때문에, 인터페이스에서는 메서드 이름 정의가 있지만, 구현 없습니다.
그리고, 자식 클래스에서는 구현합니다.
인터페이스는 클래스이름 앞에 class 대신 interface 키워드를 붙입니다.
예제 : 인터페이스 |
interface IBase1 { void Method11(); // void Mehtod2() {...} // 틀림. } interface IBase2 { void Method21(); } public class Class1 : IBase1, IBase2 { void Method11() { Debug.Log("Interface("Interface Method11"); } // 선언부가 있어야 합니다. void Method21() { Debug.Log("Interface("Interface Method21"); } // 선언부가 있어야 합니다. } |
인터페이스는 객체(인스턴스)를 생성할 수 없습니다.
인터페이스 상속을 받은 클래스를 객체(인스턴스)를 만들 수 있습니다.
예제 : 객체 생성 |
// IBase1 ib1 = new IBase1(); // 틀림. Class1 c1 = new Class1(); // 맞음. |
5. Partial 클래스
하나의 클래스에는 하나의 기능을 넣는 것을 기본으로 합니다.
하지만, 실제로 클래스를 만들다 보면 클래스가 너무 커지게 되면, 가독성을 위해서나 기능별로 분리하여 사용해야 하는 경우가 발생합니다.
이때 사용하는 것이 Partial 클래스입니다.
Partial 클래스와 Partial 클래스 멤버들은 다른 클래스를 사용하기 위해서는 접근 제한자를 public으로 사용해야 합니다.
Partial 클래스는 class앞에 partial 키워드 한정자를 넣으면 됩니다.
선언 |
public partial class 클래스-이름 {} public partial class 클래스-이름 {} |
using UnityEngine;
public partial class PartialClass
{
public void DebugLog1()
{
Debug.Log("DebugLog1");
}
}
public partial class PartialClass
{
public void DebugLog2()
{
Debug.Log("DebugLog2");
}
}
public class PartialClassExample : MonoBehaviour
{
private PartialClass m_PartialClass;
void Start()
{
m_PartialClass = new PartialClass();
m_PartialClass.DebugLog1(); // 출력 : DebugLog1
m_PartialClass.DebugLog2(); // 출력 : DebugLog2
}
}
partial 한정자는 class, struct 또는 interface 키워드 바로 앞에만 올 수 있습니다.
partial 키워드는 클래스, 구조체 또는 인터페이스의 다른 부분을 네임스페이스에서 정의할 수 있음을 나타냅니다.
모든 부분은 partial 키워드를 사용해야 합니다.
1. 구조체
구조체란 클래스와 같은 사용자 정의 데이터형입니다.
사용자 정의 데이터형란 기본 데이터 형식과 메소드로 구성 된 복합 데이터형을 의미합니다.
구조체는 클래스와 비슷한 부분이 많습니다.
하지만, 가장 큰 차이는 클래스는 참조 형식이고, 구조체는 값 형식입니다.
클래스는 **참조 형식(reference type)**으로, 메모리에서 객체의 참조(주소)를 저장합니다. 즉, 객체를 할당하면 그 객체의 데이터는 힙(heap)에 저장되고, 변수가 그 힙의 메모리 주소를 가리키는 참조를 저장합니다
public class Person
{
public string name;
public int age;
}
Person person1 = new Person();
person1.name = "Alice";
person1.age = 25;
// person2는 person1의 참조를 복사합니다.
Person person2 = person1;
// person2의 값을 수정하면 person1에도 영향을 줍니다.
person2.name = "Bob";
Console.WriteLine(person1.name); // 출력: Bob
구조체는 **값 형식(value type)**으로, 데이터 자체를 복사하여 할당합니다. 즉, 구조체 인스턴스는 스택(stack)에 저장되고, 새로운 구조체 변수는 원본 데이터의 복사본을 갖습니다.
public struct PersonStruct
{
public string name;
public int age;
}
PersonStruct person1 = new PersonStruct();
person1.name = "Alice";
person1.age = 25;
// person2는 person1의 값을 복사합니다.
PersonStruct person2 = person1;
// person2의 값을 수정해도 person1에는 영향을 주지 않습니다.
person2.name = "Bob";
Console.WriteLine(person1.name); // 출력: Alice
구조체는 클래스와 같이 메서드, 프로퍼티 등 거의 비슷한 구조를 가지고 있습니다.
new 연산자를 사용하지 않고 인스턴스화 할 수 있습니다.
생성자를 선언할 수 있으나 반드시 파라미터가 있어야 합니다.
하지만, 상속은 할 수 없습니다.
인터페이스(interface)를 구현할 수는 있습니다.
사용 방법 : 구조체 |
접근-제한자 struct 구조체-이름 { 생성자(프로퍼티) 멤버 변수 맴버 메서드 } |
using UnityEngine;
public class StructExample : MonoBehaviour
{
public struct Struct
{
public int a, b;
public Struct(int a, int b)
{
this.a = a;
this.b = b;
}
public void DebugLog()
{
Debug.LogFormat("a = {0} b = {1}", a, b);
}
}
void Start()
{
Struct str1; // new 사용하지 않고 선언
str1.a = 10;
str1.b = 20;
str1.DebugLog(); // 출력 : a = 10 b = 20
Struct str2 = new Struct(1, 2); // new 사용하여 선언
str2.DebugLog(); // 출력 : a = 1 b = 2
}
}
2. 구조체 사용
구조체는 스택이 바로 할당되기 때문에 가비지 컬렉션이 발생하지 않아서 속도면에서 시스템에 부하를 적게 주나,
구조체 내에 변수가 많으면, 크기가 제한적인 스택에 저장되기 때문에, 스택 오버플로우가 발생할 수 있습니다.
변수의 개수가 적으면(3개 정도), 구조체를 사용하면 좋습니다.
1. 제네릭 타입(Generic Type)
1.1 제네릭이란?
보통 클래스나 인터페이스, 메서드를 사용할 때, 동일한 기능을 수행하지만, 입력하는 데이터 형식만 틀린 경우가 있습니다.

그림. 기능이 동일한 클래스들
이때 매개변수를 일일히 넣어서 클래스나 인터페이스, 메서드를 만들지 않고, 제네릭 타입(Generic Type)을 사용하여 만들 수 있습니다.
동일한 기능을 하는 메서드 | 제네릭 메서드 |
void Swap(ref int x, ref int y) { var temp = y; y = x; x = temp; } void Swap(ref float x, ref float y) { var temp = y; y = x; x = temp; } |
void Swap<T>(ref T x, ref T y) { var temp = y; y = x; x = temp; } |
제네릭이란 형식 매개변수(Type parameter)입니다.
형식 매개변수를 사용해서 클래스나 메서드를 만들게 되면, 그 클래스나 메서드를 호출하기 전까지 데이터 형식 지정을 연기 할 수 있습니다.
1.2 제네릭 특징
● 코드 재사용성이 높습니다.
● 성능이 좋습니다. (런타임 때 데이터 형식이 결정 되는 것이 아니라, 코드에서 호출 할때 데이터 형식이 결정되기 때문에 성능 저하가 없습니다.)
2. 제네릭 클래스(Generic Class)
2.1 사용 방법
멤버변수의 타입을 미리 결정하지 않고, 사용할 때 제넥릭 클래스를 사용합니다.
사용방법 : 제네릭 클래스 |
public class 클래스-이름<T> { public T ... } |
using UnityEngine;
public class GenericsClassExample : MonoBehaviour
{
public class GenericsClass<T>
{
private T m_Value;
public GenericsClass(T value)
{
m_Value = value;
}
public void Method1()
{
Debug.Log(m_Value);
}
}
void Start()
{
GenericsClass<int> m_GenericsClass1 = new GenericsClass<int>(5);
m_GenericsClass1.Method1(); // 출력 : 5
GenericsClass<float> m_GenericsClass2 = new GenericsClass<float>(5.1f);
m_GenericsClass2.Method1(); // 출력 : 5.1
}
}
2.2 C# 제네릭 클래스
C#는 기본적으로 제공하는 제네릭 클래스가 많습니다.
대표적인 것이 List, Dictionary, LinkedList 등이 있습니다.
2.3 제네릭 타입 제약 (Type Constraint)
제네릭 타입을 선언할 때 제약 조건을 걸 수 있습니다.
사용방법은 제네릭 선언 후 where T : 제약조건 과 같은 식으로 where 뒤에 제약 조건을 붙이면 됩니다.
2.3.1 new 제약 조건
new 제약 조건은 제네릭 클래스 선언의 형식 인수에 공용 매개변수가 없는 생성자가 있어야 함을 지정합니다.
사용 방법 : 제네릭 new 타입 제약1 |
class Class1<T> where T : new() // T는 디폴트 생성자를 가져야 함 |
다른 제약 조건과 함께 new() 제약 조건을 사용하는 경우 마지막에 지정해야 합니다.
사용 방법 : 제네릭 new 타입 제약2 |
class Class1<T> where T : IComparable, new() // T는 디폴트 생성자를 가져야 함 |
2.3.2 형식에 대한 조건
사용 방법 : 제네릭 형식에 대한 조건 |
class Class1<T> where T : struct // T는 Value 타입임 class Class2<T> where T : class // T는 Reference 타입임 class Class4<T> where T : Class3 // T는 Class3의 파생클래스어야 함 class Class4<T> where T : IComparable // T는 IComparable 인터페이스를 가져야 함. class Class5<T, U> // 복수 타입의 매개변수 제약 where T : class where U : struct { } |
3. 제네릭 메서드
매개변수의 타입을 미리 결정하지 않고, 사용시 결정하는 것입니다.
사용방법 : 제네릭 메서드 |
T 메소드-이름<T>(T arg) { T temp = arg; //... return temp; } |
using UnityEngine;
public class GenericExample : MonoBehaviour
{
void Start()
{
int iX = 1, iY = 2;
Swap(ref iX, ref iY);
Debug.LogFormat($"x = {iX} y = {iY}"); // 출력 : x = 2 y = 1
string sX = "ab", sY = "cd";
Swap(ref sX, ref sY);
Debug.LogFormat($"x = {sX} y = {sY}"); // 출력 : x = cd y = ab
}
void Swap<T>(ref T x, ref T y)
{
var temp = y;
y = x;
x = temp;
}
}
직렬화(Serialization)와 역직렬화(Deserialization)에 대해 더 자세히 설명하겠습니다. 이 과정들은 객체 지향 프로그래밍에서 데이터의 저장과 전송을 효율적으로 처리하는 데 필수적입니다.
1. 직렬화 (Serialization)
직렬화는 객체의 상태를 특정 형식으로 변환하여 저장하거나 전송할 수 있게 하는 과정입니다. 이 과정에서 객체의 속성과 데이터를 바이트 스트림이나 문자열 형식으로 변환합니다. 직렬화는 다양한 형태로 이루어질 수 있으며, 가장 일반적으로 사용되는 포맷은 JSON, XML, 바이너리 포맷이다. 게임에서 플레이어의 정보를 저장해야 할 때가 있다. 플레이어의 이름과 점수를 저장하는 Player 객체를 예로 들어보자
[System.Serializable]
public class Player
{
public string Name;
public int Score;
}
// 객체 생성
Player player = new Player();
player.Name = "Alice";
player.Score = 100;
// 직렬화
string json = JsonUtility.ToJson(player); // Player 객체를 JSON 문자열로 변환
위 코드를 실행하면, player 객체는 JSON 형식의 문자열로 변환된다.
{"Name":"Alice","Score":100}
이 문자열은 파일에 저장하거나 네트워크를 통해 전송할 수 있다.
2. 역직렬화 (Deserialization)
저장된 JSON 문자열을 다시 Player 객체로 복원할 수 있습니다. 다음은 역직렬화 과정이다.
// 역직렬화
Player loadedPlayer = JsonUtility.FromJson<Player>(json); // JSON 문자열을 Player 객체로 변환
이제 loadedPlayer 객체는 원래의 player 객체와 동일한 상태를 가지게 된다:
- loadedPlayer.Name = "Alice"
- loadedPlayer.Score = 100
요약하면 직렬화는 객체를 바이트 스트림이나 문자열 형식으로 변환하여 저장하거나 전송 가능하게 하는 과정이고 역직렬화는 저장된 데이터(예: JSON 문자열)를 다시 원래의 객체로 복원하는 과정이다. 위의 예시에서는 JSON 문자열을 Player 객체로 변환했다. 이러한 과정을 통해 객체의 상태를 쉽게 저장하고 복원할 수 있다.
3. SerializeField
직렬화는 Unity에서 객체의 상태를 특정 형식으로 변환하여 저장하거나 전송할 수 있게 하는 과정으로, 씬 파일이나 prefab과 같은 자산에 포함되어 나중에 다시 로드됩니다. 반면, 인스펙터는 Unity 에디터 내에서 게임 오브젝트와 컴포넌트의 속성을 시각적으로 조작할 수 있는 패널입니다. 인스펙터에 필드를 노출시키기 위해서는, public 필드는 자동으로 노출되지만 private 필드는 기본적으로 표시되지 않으며, 이때 [SerializeField] 속성을 사용하여 private 필드도 인스펙터에 노출할 수 있습니다. 인스펙터에서 수정된 값은 직렬화를 통해 씬 파일이나 prefab에 저장되며, 게임이 다시 로드될 때 역직렬화 과정을 통해 원래 객체의 상태로 복원됩니다. 이러한 관계 덕분에 개발자는 코드를 수정하지 않고도 게임 오브젝트의 속성을 쉽게 조정하고 테스트할 수 있어, 빠른 프로토타입 제작과 디버깅이 가능해집니다.
[System.Serializable]
public class PlayerData
{
public string playerName;
public int playerScore;
}
[System.Serializable]은 클래스를 직렬화 가능하게 만드는 특성으로, 클래스 선언 앞에 추가함으로써 해당 클래스의 모든 필드가 직렬화될 수 있도록 표시합니다. 이 특성을 사용하면 객체의 상태를 JSON, XML 또는 바이너리 형식으로 변환하여 저장하거나 전송할 수 있으며, 나중에 역직렬화를 통해 원래 객체로 복원할 수 있습니다. 이를 통해 Unity와 같은 환경에서 게임 오브젝트의 상태를 유지하고 관리하는 데 유용합니다.
이 코드에서 PlayerData 클래스는 직렬화 가능하므로, 해당 객체를 파일에 저장하거나 네트워크를 통해 전송할 수 있습니다.
유니티 enum 이란?
유니티 enum 이란 C#에서 사용하는 열거형(“enumeration”의 줄임말) 형식을 가리킵니다. 열거형이란 이름이 지정된 상수 집합을 나타내는 값 형식입니다.
열거형은 enum 키워드를 사용하여 정의되며 상수는 쉼표로 구분된 값의 목록으로 지정됩니다. 다음은 enum 의 예제 코드입니다.
public enum Days
{
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday
}
열거형은 일련의 지정된 상태(state) 또는 값의 집합을 나타내는 데 자주 사용됩니다. 예를 들어 열거형을 사용하여 위의 예에서와 같이 요일을 나타낼 수 있습니다.
유니티 enum 의 이점
enum, 즉 열거형을 사용하면 일반 상수를 사용하는 것보다 몇 가지 이점을 누릴 수 있습니다.
읽기 쉽다
열거형이 더 읽기 쉽습니다. 열거형은 숫자 상수보다 이해하기 쉬운, 지정된 이름을 가진 상수를 사용합니다. 예를 들어 Days.Sunday 라고 하면 우리는 이 값이 일요일을 나타낸다는 것을 직관적으로 알 수 있습니다.
유지 관리가 쉽다
열거형은 유지 관리가 더 쉽습니다. 열거형에서 상수를 추가, 제거 또는 재정렬해야 하는 경우 상수 값을 변경하지 않고도 수행할 수 있습니다. 이는 상수가 코드의 여러 위치에서 사용되는 경우 특히 유용할 수 있습니다.
안전하다
열거형은 안전합니다(즉, type-safe). 열거형은 미리 지정된 type 이므로 열거형 변수에 실수로라도 잘못된 유형의 값을 할당할 수 없습니다. 따라서 열거형을 사용하면 코드의 오류를 사전에 방지할 수 있습니다.
유니티 enum 사용의 예
다음은 유니티에서 열거형 enum 을 사용하여 코딩한 예입니다.
using UnityEngine;
public class EnumExample : MonoBehaviour
{
// 3개의 State 값을 열거형으로 지정
public enum States
{
Idle,
Walking,
Running
}
// enum 타입의 변수를 선언
public States currentState;
void Update()
{
// switch 구문을 이용하여 열거형 변수를 사용
switch (currentState)
{
case States.Idle:
// state 가 idle 일 때 콘솔 화면에 상태를 표시
Debug.Log("Idle");
break;
case States.Walking:
// state 가 walking 일 때 콘솔 화면에 상태를 표시
Debug.Log("Walking");
break;
case States.Running:
// state 가 running 일 때 콘솔 화면에 상태를 표시
Debug.Log("Running");
break;
}
}
}
위의 코드를 하나씩 살펴 보도록 하겠습니다.
public enum States
{
Idle,
Walking,
Running
}
여기에서는 Idle, Walking 및 Running의 세 가지 값이 있는 States라는 새 열거형(enum)을 정의하였습니다.
public States currentState;
다음으로 앞에서 정의한 열거형 States 타입의 퍼블릭 변수인 currentState 를 선언하였습니다. 이 currentState 변수에는 앞의 States 열거형에 정의된 세 값(Idle, Walking, Running) 중 하나를 지정할 수 있습니다.
switch (currentState)
{
case States.Idle:
Debug.Log("Idle");
break;
case States.Walking:
Debug.Log("Walking");
break;
case States.Running:
Debug.Log("Running");
break;
}
이 코드 블록은 switch 문을 사용하여 currentState 변수의 값에 따라 다른 코드를 실행합니다. 여기에서는 각각의 상태에 따라 유니티의 콘솔창에 Idle, Walking, Running 이라는 글자를 표시합니다.
열거형이 아닌 상수를 사용한 예제
위의 유니티 예제 코드는 다음과 같은 방식으로도 작성할 수 있습니다. 이 경우에는 열거형이 아닌 일반 상수를 이용하여 똑 같은 동작을 수행하는 코드를 작성한 것입니다.
열거형을 사용한 코드와 비교해서 어떤 점이 다르고 같은지 살펴 보시기 바랍니다.
using UnityEngine;
public class ConstantExample : MonoBehaviour
{
// 3가지 State 에 대한 3개의 상수 선언
public const int Idle = 0;
public const int Walking = 1;
public const int Running = 2;
// 현재 State 를 저장할 정수 타입의 변수 선언
public int currentState;
void Update()
{
// switch 구문을 이용하여 위에서 만든 상수 사용
switch (currentState)
{
case Idle:
Debug.Log("Idle");
break;
case Walking:
Debug.Log("Walking");
break;
case Running:
Debug.Log("Running");
break;
}
}
}
열거형 enum 을 사용하건, 일반 상수를 사용하건 동일한 작업을 수행하는 코드를 작성할 수 있습니다. 하지만 열거형을 사용하는 것에 장점이 더 많으므로 위와 같은 경우에는 가급적 열거형을 사용하는 것이 좋습니다.
'Development > 3D Engine Programming' 카테고리의 다른 글
Unity Programming [5] : 유니티 소프트웨어 설계 (Attribute, Gizmos) (1) | 2024.12.27 |
---|---|
Unity Programming [4] : AI 통합 게임 엔진 (AI-Integrated Game Engine) (3) | 2024.11.19 |
Unity Programming [3] : 시스템 로직 (Unity System Logics) (2) | 2024.11.15 |
Unity Programming [2] : C# 입출력 연산 (Input/Output (I/O) operations) (2) | 2024.11.08 |
Unity Programming [1]: 병행성 추상화 프레임워크 (Concurrency Abstraction Framework) (0) | 2024.10.28 |