Unity 사전캠프 6일차 - C# 기초 문법

한예준·2025년 3월 24일

처음 C#을 배우며 가장 먼저 접하는 문법들을 정리했다. Unity에 어떻게 적용될지를 생각하며 학습을 진행했다.


입력과 출력

  • Console.WriteLine() → 콘솔에 문자열 출력
  • Console.ReadLine() → 사용자 입력 받기 (항상 문자열로 입력됨)
  • 숫자를 입력받고 싶으면 int.Parse() 또는 Convert.ToInt32() 사용
Console.WriteLine("이름을 입력하세요:");
string name = Console.ReadLine();
Console.WriteLine("안녕하세요, " + name + "님!");

Console.WriteLine("나이를 입력하세요:");
int age = int.Parse(Console.ReadLine());
Console.WriteLine("당신의 나이는 " + age + "세입니다.");

조건문 (if / else if / else)

조건에 따라 흐름을 분기할 때 사용.

int age = 20;

if (age >= 65)
{
    Console.WriteLine("노년층입니다.");
}
else if (age >= 18)
{
    Console.WriteLine("성인입니다.");
}
else
{
    Console.WriteLine("미성년자입니다.");
}

반복문 (for / while / do-while)

동일한 동작을 반복할 때 사용.

for문

for (int i = 0; i < 5; i++)
{
    Console.WriteLine("for 반복: " + i);
}

while문

int i = 0;
while (i < 5)
{
    Console.WriteLine("while 반복: " + i);
    i++;
}

do-while문

int i = 0;
do
{
    Console.WriteLine("do-while 반복: " + i);
    i++;
} while (i < 5);

for: 반복 횟수가 정해졌을 때 적합
while: 조건이 참인 동안 반복
do-while: 최소 1회는 실행 보장


배열 (Array)

배열이란?

  • 같은 타입의 데이터 묶음
  • 한 번 선언하면 크기 고정
  • 인덱스로 각 요소에 접근 (0부터 시작)

선언과 초기화

선언만 하기
int[] numbers = new int[5]; // 크기 5짜리 int 배열
선언과 동시에 초기화
int[] numbers = { 1, 2, 3, 4, 5 };
new 키워드로 초기화
string[] fruits = new string[] { "apple", "banana", "orange" };

배열 요소 접근

int[] scores = { 10, 20, 30 };

Console.WriteLine(scores[0]); // 10
scores[1] = 50;
Console.WriteLine(scores[1]); // 50

순회

for (int i = 0; i < numbers.Length; i++)
{
    Console.WriteLine(numbers[i]);
}

foreach (int num in numbers)
{
    Console.WriteLine(num);
}

추가사항 - 배열 vs 컬렉션

공통점

  • 둘 다 여러 개의 데이터를 저장하는 데 사용
  • 반복문으로 순회 가능 (for, foreach)
  • 같은 타입의 데이터를 다룰 수 있음 (List<int>, int[] 등)
구분배열 (Array)컬렉션 (List 등)
크기고정유동적
기능제한적삽입/삭제 유연
타입단일 타입제네릭 기반

List 예시

List<int> scores = new List<int>();
scores.Add(100);
scores.Add(80);
Console.WriteLine(scores[0]); // 100

→ 배열은 단순하고 빠르지만, List는 더 유연해서 실무에서 더 자주 쓰임.


클래스

클래스는 C#과 Unity 개발에서 가장 기본이자 핵심이 되는 개념이다. 데이터를 담는 그릇이자, 행동을 정의하는 틀. 게임 오브젝트 하나하나가 결국 클래스라는 점에서 반드시 정확히 이해하고 넘어가야 한다.


클래스란?

  • 속성과 기능을 하나로 묶는 설계도
  • 객체를 만들기 위한 기반
  • Unity의 MonoBehaviour도 클래스

참고: MonoBehaviour는 Unity에서 스크립트를 오브젝트에 부착할 수 있게 해주는 특별한 클래스입니다. 이걸 상속받아야 Unity가 Start(), Update(), OnTriggerEnter() 같은 메서드를 자동으로 호출해줍니다. 게임 오브젝트와 연결되는 모든 스크립트는 MonoBehaviour를 상속받아야 하며, 이를 통해 transform, gameObject, GetComponent() 같은 엔진 기능도 사용할 수 있게 됩니다.

public class Player
{
    public string name;
    public int hp;

    public Player(string name)
    {
        this.name = name;
        hp = 100;
    }

    public void ShowStatus()
    {
        Console.WriteLine(name + "의 체력: " + hp);
    }
}

Player p = new Player("루크");
p.ShowStatus();

클래스의 구성 요소

  • 필드: 데이터를 저장 (예: name, hp)
  • 메서드: 동작 정의 (예: ShowStatus)
  • 생성자: 객체 생성 시 자동 호출되는 특별한 메서드

클래스와 객체의 관계

  • 클래스는 틀
  • 객체는 그 틀로 만들어진 실제 물건
  • 같은 클래스라도 여러 객체 생성 가능

게임 개발에서 클래스가 중요한 이유

모든 게임 오브젝트는 "객체"다

캐릭터, 몬스터, 총알, 아이템, 스킬, UI 버튼까지…
게임에 나오는 거의 모든 요소는 속성과 동작을 가진 오브젝트다.

이런 오브젝트들을 체계적으로 다루기 위해선 설계도(클래스)가 필요하다.

public class Enemy
{
    public string name;
    public int hp;

    public void TakeDamage(int dmg)
    {
        hp -= dmg;
        Console.WriteLine(name + "이(가) 피해를 입었습니다. 남은 체력: " + hp);
    }
}

→ 이 Enemy 클래스로 몬스터를 수십 마리 만들 수 있다.


코드 재사용 & 유지보수가 쉬움

예를 들어 몬스터가 여러 종류 있는데, 공통 동작은 비슷하고 일부만 다를 때:

public class Monster
{
    public int hp;
    public virtual void Attack() { /* 기본 공격 */ }
}

public class Goblin : Monster
{
    public override void Attack() { /* 고블린 특수 공격 */ }
}

→ 공통 기능은 상속으로 묶고, 특수 기능은 오버라이딩(재정의) 하면 편하다.


Unity에서 모든 컴포넌트는 클래스 기반

Unity에서 MonoBehaviour를 상속한 모든 스크립트는 클래스다.

즉, PlayerController, EnemyAI, ItemPickup 이런 스크립트들 전부 클래스 기반이고, Unity는 이 클래스들을 바탕으로 컴포넌트 시스템을 굴리는 것이다.

public class PlayerController : MonoBehaviour
{
    void Update()
    {
        Move();
    }

    void Move()
    {
        // 입력 받아서 이동 처리
    }
}

게임 씬의 계층 구조 = 클래스 간 관계

씬에 배치된 오브젝트들이 서로 어떻게 상호작용하는지 = 클래스들 간의 관계

플레이어 → 무기 → 총알, 플레이어 → UI, 적 → 상태머신 등
모든 관계가 클래스 간의 구조로 표현된다.


디자인 패턴과 클래스

상태 머신, 싱글톤, 팩토리 패턴 같은 게임에서 자주 쓰는 구조들도 전부 클래스를 전제로 한 설계 패턴이다.

예: 게임 매니저는 대부분 싱글톤 클래스

public class GameManager : MonoBehaviour
{
    public static GameManager Instance;

    void Awake()
    {
        if (Instance == null) Instance = this;
    }
}

정리하자면
게임 요소클래스가 하는 일
플레이어/몬스터상태, 행동 정의
무기/아이템속성, 효과 정의
UI/이벤트 관리데이터 저장 + 인터랙션 처리
전체 게임 흐름매니저 클래스, 싱글톤 등 구조 관리

→ 게임을 만든다는 건 결국 객체 간 상호작용을 설계하는 것,
→ 그리고 그 객체들의 설계도가 바로 클래스다.

  • 코드를 구조화하고 역할을 나눌 수 있음 (플레이어, 적, 무기 등)
  • 재사용성과 확장성이 높음 (공통 기능을 부모 클래스로 만들기 등)
  • 유지보수가 쉬움 (버그 수정, 기능 추가가 명확해짐)
  • Unity 기준: MonoBehaviour를 상속받은 스크립트 = 결국 클래스
  • 게임 내 오브젝트, UI, 컴포넌트 모두 클래스로 구성됨
  • 필드: 인스펙터에서 설정 가능
  • 메서드: 동작 정의 (Start, Update, 커스텀 함수 등)

실전 예시 - 자동차 클래스

public class Car
{
    public string brand;
    public int speed;

    public void Drive()
    {
        Console.WriteLine(brand + "가 " + speed + "km/h로 달립니다.");
    }
}

Car myCar = new Car();
myCar.brand = "현대";
myCar.speed = 100;
myCar.Drive();

클래스는 객체지향의 출발점이자, Unity 스크립트의 핵심이다.
다양한 오브젝트를 구조화하고, 재사용성과 확장성을 확보하려면 반드시 익숙해져야 할 개념. 이걸 제대로 이해해야 이후에 나올 상속, 인터페이스, 추상 클래스, 가상 메서드도 자연스럽게 연결된다.


상속

상속(Inheritance)은 객체지향 프로그래밍의 핵심 개념 중 하나로, 기존 클래스의 설계를 재사용하고 확장할 수 있게 해준다. 게임처럼 다양한 오브젝트가 존재하는 환경에서는 코드 중복을 줄이고 관리하기 위해 반드시 이해해야 할 개념이다.


상속이란?

  • 기존 클래스(부모)의 기능을, 새로운 클래스(자식)가 물려받아 사용하는 것
  • 코드 재사용을 가능하게 함
  • 계층 구조 설계에 적합함 (예: 일반 → 구체화)
public class Animal // 부모 클래스
{
    public string name;

    public void Breathe()
    {
        Console.WriteLine(name + "가 숨을 쉽니다.");
    }
}

public class Dog : Animal // Animal을 상속받은 자식 클래스
{
    public void Bark()
    {
        Console.WriteLine("멍멍!");
    }
}

Dog d = new Dog();
d.name = "초코";
d.Breathe();  // 초코가 숨을 쉽니다.
d.Bark();     // 멍멍!

상속의 장점

1. 코드 중복 제거
// 중복된 클래스들
public class Goblin {
    public int hp;
    public void Die() { Console.WriteLine("죽음"); }
}
public class Orc {
    public int hp;
    public void Die() { Console.WriteLine("죽음"); }
}

// 상속으로 정리
public class Enemy {
    public int hp;
    public void Die() { Console.WriteLine("죽음"); }
}

public class Goblin : Enemy { }
public class Orc : Enemy { }
2. 기능 확장
public class Goblin : Enemy
{
    public void ThrowStone()
    {
        Console.WriteLine("돌을 던졌다!");
    }
}
3. 메서드 재정의 (Override)
public class Enemy {
    public virtual void Attack() {
        Console.WriteLine("기본 공격");
    }
}

public class Orc : Enemy {
    public override void Attack() {
        Console.WriteLine("도끼 공격!");
    }
}
  • virtual: 부모 클래스에서 재정의 가능하다고 표시
  • override: 자식 클래스에서 해당 메서드를 새롭게 구현

접근 제어자와 상속

구분설명상속 가능성
public class외부 공개✅ 가능
class (생략)internal (같은 프로젝트에서만 사용)⚠️ 제한적 가능
private class클래스 내부에서만 사용❌ 불가

상속 가능 여부는 클래스의 "접근 제한자"에 따라 달라진다.

Unity에서는 대부분의 스크립트를 public class로 작성한다. 그래야 다른 스크립트에서 불러오거나 상속할 수 있으며, MonoBehaviour를 상속한 Unity 컴포넌트 스크립트들도 모두 public으로 선언된다.. 그래야 다른 스크립트에서 불러오거나 상속 가능.


private을 사용하는 이유

C#에서 private은 클래스 내부에서만 멤버를 사용할 수 있도록 제한하는 접근 제어자다. Unity에서도 private은 매우 자주 사용되며, 다음과 같은 이유에서 활용된다:

  1. 데이터 보호

    • 외부에서 직접 접근해 잘못된 값으로 바꾸지 못하게 막기 위함
    public class Player
    {
        private int hp = 100;
    
        public void TakeDamage(int damage)
        {
            hp -= damage;
            if (hp < 0) hp = 0;
        }
    }
  2. 캡슐화 (Encapsulation)

    • 내부 구현은 숨기고, 외부엔 필요한 기능만 노출
    public class Enemy
    {
        private int hp;
    
        public void SetHp(int value)
        {
            if (value >= 0) hp = value;
        }
    
        public int GetHp()
        {
            return hp;
        }
    }
  3. 유지보수성 향상

    • 불필요한 외부 의존을 줄여, 추후 코드 수정이 편해짐
  4. Unity 기준 사용 예

    • private 변수는 인스펙터에 안 보임
    • [SerializeField] private 조합을 사용하면 인스펙터에 보이면서도 외부 접근은 차단 가능

private 멤버와 상속

  • 클래스는 상속되지만, private 멤버는 자식 클래스에서 직접 접근할 수 없음
  • 대신 protected를 사용하면 자식 클래스에서도 접근 가능
public class Character {
    private int hp; // ❌ 자식 클래스에서 접근 불가
    protected int mp; // ✅ 자식 클래스에서 접근 가능
}

Unity에서 자주 쓰이는 상속 예시

캐릭터 클래스 구조화
public class Character
{
    public string name;
    public int hp;

    public virtual void Attack()
    {
        Console.WriteLine(name + " 기본 공격!");
    }
}

public class Warrior : Character
{
    public override void Attack()
    {
        Console.WriteLine(name + " 검으로 공격!");
    }
}

public class Mage : Character
{
    public override void Attack()
    {
        Console.WriteLine(name + " 파이어볼!");
    }
}
다형성으로 활용하기
Character c1 = new Warrior();
c1.name = "가로쉬";
c1.Attack(); // 가로쉬 검으로 공격!

Character c2 = new Mage();
c2.name = "제이나";
c2.Attack(); // 제이나 파이어볼!

→ 부모 타입(Character)으로 자식 클래스(Warrior, Mage)를 다룰 수 있는 게 다형성(polymorphism)의 시작이다. 다형성을 활용하면 공통된 코드에서 다양한 동작을 유연하게 실행할 수 있어, 코드 구조가 더 깔끔하고 유지보수가 쉬워진다.


상속은 재사용성, 유지보수성, 확장성 모든 면에서 강력한 도구다. Unity에서도 MonoBehaviour를 기반으로 상속을 사용하는 일이 많기 때문에, 이 개념을 확실히 이해해두면 구조적인 게임 설계에 큰 도움이 된다.


인터페이스

인터페이스(Interface)는 "공통 기능을 가진 클래스들이 같은 메서드 구조를 따르도록 강제"하는 장치다.
특히 Unity처럼 다양한 오브젝트가 공존하는 환경에서는 코드의 일관성과 유연성을 높이는 데 필수적인 개념이다.


인터페이스란?

  • 클래스가 반드시 구현해야 하는 메서드의 틀만 정의한 것
  • 실제 구현(내용)은 없음 → 구현은 각 클래스가 직접 담당
  • 하나의 클래스가 여러 개의 인터페이스를 구현할 수 있음 (다중 구현 가능)
public interface IDamageable
{
    void TakeDamage(int damage);
}

public class Enemy : MonoBehaviour, IDamageable
{
    public void TakeDamage(int damage)
    {
        Debug.Log("적이 " + damage + "만큼 피해를 입었다!");
    }
}

public class Player : MonoBehaviour, IDamageable
{
    public void TakeDamage(int damage)
    {
        Debug.Log("플레이어가 " + damage + "만큼 피해를 입었다!");
    }
}

IDamageable을 구현한 클래스는 모두 TakeDamage(int) 메서드를 반드시 구현해야 한다.

이런 구조를 통해 다양한 객체에서 동일한 행동을 보장할 수 있다.


인터페이스의 주요 특징

특징설명
강제성인터페이스를 구현한 클래스는 정의된 모든 메서드를 반드시 구현해야 함
다중 구현하나의 클래스가 여러 인터페이스를 동시에 구현 가능
구현 없음메서드 본문 없이 이름, 매개변수만 정의됨
접근 제한자 생략인터페이스 내부 메서드는 항상 public으로 간주됨

Unity 실전 예시 - 인터페이스 기반 상호작용

public interface IInteractable
{
    void Interact();
}

public class Door : MonoBehaviour, IInteractable
{
    public void Interact()
    {
        Debug.Log("문 열림");
    }
}

public class NPC : MonoBehaviour, IInteractable
{
    public void Interact()
    {
        Debug.Log("대화 시작");
    }
}
// 공통 기능 호출
void InteractWithTarget(GameObject target)
{
    IInteractable obj = target.GetComponent<IInteractable>();
    if (obj != null)
    {
        obj.Interact();
    }
}

→ 다양한 오브젝트를 인터페이스 하나로 통합하여 제어할 수 있다.
→ 코드 의존도를 낮추고, 기능을 "묶는 기준"으로 삼을 수 있다.

Unity에서는 인터페이스를 통해 다양한 컴포넌트를 범용적으로 가져올 수 있다.


인터페이스를 사용하는 이유와 프로그래머에게 유용한 이유

인터페이스는 플레이어에게 직접적으로 드러나지는 않지만, 개발자에게는 설계를 구성하는 가장 강력한 도구 중 하나다.

1) 설계 명확성
  • 인터페이스는 기능에 대한 '계약서' 역할을 한다.
  • 팀 작업에서 명확한 구현 기준이 되어 협업 효율성을 높인다.
  • 클래스가 제공해야 할 기능이 명확하게 드러난다.
2) 클래스 간 결합도 낮춤 (Low Coupling)
  • 클래스 간 직접 참조 없이 인터페이스만 통해 상호작용 가능
  • 예: IDamageable 인터페이스가 있으면 Player, Enemy, Boss 모두 동일하게 처리 가능
  • → 코드 재사용성과 확장성이 좋아진다.
3) 유지보수 용이
  • 인터페이스는 고정하고 구현 클래스만 바꾸면 됨
  • 예: DummyEnemyOrcEnemy로 쉽게 교체 가능
4) 다양한 타입을 하나로 다룸
  • List<IInteractable>처럼 다양한 객체를 한 컬렉션으로 관리 가능
  • GetComponent<IInterface>() 방식으로 Unity 오브젝트에서 인터페이스 구현체를 통합 접근
5) 테스트 코드 작성에 유리
  • 테스트용 가짜(Mock) 클래스로도 동일한 인터페이스 구현 가능
  • 게임 로직 테스트 시 실제 게임 오브젝트 없이 진행 가능
6) 기능 분리와 구조적인 코드 설계
  • 클래스는 필요한 인터페이스만 선택적으로 구현하여 책임을 나눌 수 있음
  • 인터페이스 중심의 구조는 유지보수성과 확장성을 동시에 확보할 수 있다

인터페이스는 설계 도구로서의 역할이 매우 강력하다.
특히 Unity처럼 다양한 오브젝트가 존재하고 기능이 겹치는 상황에서 인터페이스를 잘 활용하면, 코드의 유연성, 재사용성, 유지보수성을 모두 높일 수 있다.


추상 클래스

추상 클래스(Abstract Class)는 "공통되는 속성과 기능은 미리 구현하고, 나머지 구현은 자식 클래스에게 맡기는 설계 도구"다. 인터페이스와 비슷하지만 일부 구현이 포함될 수 있다는 점에서 차별화되며, 기능이 유사한 클래스들을 설계할 때 매우 유용하다.


추상 클래스란?

기본 문법
// 추상 클래스 선언
public abstract class 클래스이름
{
    // 일반 메서드 (공통 구현 가능)
    public void 일반메서드() { ... }

    // 추상 메서드 (자식이 반드시 구현해야 함)
    public abstract void 메서드이름();
}

// 자식 클래스에서 상속 후 구현
public class 자식클래스 : 클래스이름
{
    public override void 메서드이름()
    {
        // 구체적인 동작 구현
    }
}
  • 하나 이상의 추상 메서드(abstract method)를 가지는 클래스
  • 직접 인스턴스를 만들 수 없다 (객체 생성 불가)
  • 자식 클래스가 반드시 구현해야 하는 메서드와 공통 구현을 함께 담을 수 있음
public abstract class Character
{
    public string name;

    public void Move()
    {
        Console.WriteLine(name + "이(가) 이동합니다.");
    }

    public abstract void Attack();
}

public class Warrior : Character
{
    public override void Attack()
    {
        Console.WriteLine(name + "이(가) 검으로 공격!");
    }
}

Warrior w = new Warrior();
w.name = "루크";
w.Move();   // 루크이(가) 이동합니다.
w.Attack(); // 루크이(가) 검으로 공격!

→ 공통 기능은 부모 클래스에서 구현하고, 개별 동작은 자식 클래스에서 구현한다.


추상 클래스의 특징

항목설명
인스턴스 생성 여부❌ 직접 생성 불가
추상 메서드 포함 여부✅ 최소 1개 이상 필요
공통 메서드 구현✅ 가능
필드/속성 가짐✅ 가능
생성자 사용 가능✅ 가능
상속 수 제한❌ 단일 상속만 가능

추상 클래스 vs 인터페이스

항목추상 클래스인터페이스
구현 포함✅ 일부 구현 가능❌ 구현 불가능
필드/속성✅ 가능❌ 없음
생성자✅ 가능❌ 없음
다중 상속❌ 불가능✅ 가능
사용 목적공통 기능 + 개별 구현 구분 목적기능 명세(구조) 강제화 목적

추상 클래스는 "서로 관련 있는 클래스 간 공통 기능을 묶을 때",
인터페이스는 "다른 계열이라도 같은 동작을 하게 만들 때" 사용하면 좋다.

비유로 정리하면:

  • 인터페이스: "계약서" — "이 기능 꼭 만들어주세요!"
  • 추상 클래스: "공통 로직 + 설계도" — "공통 기능은 미리 넣어둘게, 나머진 네가 완성해."

실전 예시 - 다양한 캐릭터 설계

public abstract class Character
{
    public string name;

    public void Move()
    {
        Console.WriteLine(name + " 이동!");
    }

    public abstract void Attack();
}

public class Mage : Character
{
    public override void Attack()
    {
        Console.WriteLine(name + " 파이어볼!");
    }
}

public class Archer : Character
{
    public override void Attack()
    {
        Console.WriteLine(name + " 활 공격!");
    }
}

Character mage = new Mage();
mage.name = "제이나";
mage.Move();   // 제이나 이동!
mage.Attack(); // 제이나 파이어볼!

언제 추상 클래스를 쓰는가?

  • 자식 클래스들이 같은 속성과 동작을 공유할 때
  • 예: Move, Die는 부모에서 구현, Attack은 자식이 개별 구현할 경우

기본 구현 + 커스터마이징이 동시에 필요한 상황에 적합


추상 클래스는 공통 기능을 묶으면서도, 개별 클래스로 구체적인 기능을 나누고자 할 때 매우 유용한 도구다.
유지보수가 쉬운 구조와 깔끔한 상속 설계를 위해 꼭 익혀야 할 객체지향 개념 중 하나다.


가상 메서드

가상 메서드(Virtual Method)는 부모 클래스에서 기본 동작을 정의하되, 자식 클래스가 원하면 그 동작을 바꿀 수 있도록 허용하는 메서드다. 이는 상속과 다형성을 효과적으로 활용할 수 있게 해주는 C#의 핵심 기능 중 하나다.


가상 메서드란?

  • virtual 키워드로 정의된 부모 클래스의 메서드
  • 자식 클래스에서 override 키워드를 사용해 재정의 가능
  • 재정의하지 않으면 기본 동작 그대로 사용됨 (선택적 오버라이드)
기본 문법
public class Enemy
{
    public virtual void Attack()
    {
        Console.WriteLine("기본 공격!");
    }
}

public class Orc : Enemy
{
    public override void Attack()
    {
        Console.WriteLine("도끼 공격!");
    }
}

Enemy e1 = new Enemy();
e1.Attack(); // 기본 공격!

Enemy e2 = new Orc();
e2.Attack(); // 도끼 공격!

→ 부모 클래스에서 기본 기능을 제공하고, 자식 클래스는 필요할 때만 재정의하면 됨.


가상 메서드 vs 일반 메서드

가상 메서드 vs 일반 메서드 비교
항목가상 메서드 (virtual)일반 메서드
재정의 가능 여부✅ 자식 클래스에서 override 가능❌ 재정의 불가 (숨기거나 별도 구현해야 함)
다형성 활용✅ 가능❌ 제한적
동작 확장성유연하게 커스터마이징 가능고정된 구현만 가능
예시 코드
public class Enemy
{
    public void NormalAttack()
    {
        Console.WriteLine("기본 공격!");
    }

    public virtual void SpecialAttack()
    {
        Console.WriteLine("기본 특수 공격!");
    }
}

public class Boss : Enemy
{
    // 일반 메서드는 숨기기 (new 키워드 필요)
    public new void NormalAttack()
    {
        Console.WriteLine("강력한 일반 공격!");
    }

    // 가상 메서드는 재정의 가능
    public override void SpecialAttack()
    {
        Console.WriteLine("궁극기 발동!");
    }
}

Enemy boss = new Boss();
boss.NormalAttack();   // 기본 공격!
boss.SpecialAttack();  // 궁극기 발동!

→ 일반 메서드는 숨기기(new), 가상 메서드는 재정의(override). 부모 타입으로 호출할 경우 가상 메서드만 제대로 동작 확장됨.


실전 예시 - 다양한 적 캐릭터 공격 방식

public class Enemy
{
    public string name;

    public virtual void Attack()
    {
        Console.WriteLine(name + "이(가) 기본 공격!");
    }
}

public class Goblin : Enemy
{
    public override void Attack()
    {
        Console.WriteLine(name + "이(가) 돌을 던진다!");
    }
}

public class Skeleton : Enemy
{
    // override하지 않음 → 기본 공격 그대로 사용
}

Enemy goblin = new Goblin();
goblin.name = "고블린";
goblin.Attack(); // 고블린이(가) 돌을 던진다!

Enemy skeleton = new Skeleton();
skeleton.name = "스켈레톤";
skeleton.Attack(); // 스켈레톤이(가) 기본 공격!

→ 같은 부모 타입으로 다룰 수 있으면서도, 행동은 클래스에 따라 다르게 할 수 있다.


언제 가상 메서드를 쓰는가?

  • 공통 기능은 기본 구현하고, 일부 클래스만 다르게 동작하게 하고 싶을 때
  • 전체 로직 흐름은 동일하지만, 중간 동작을 확장 가능하게 만들고 싶을 때
  • 기본 동작을 유지하되, 특별한 케이스만 재정의하고 싶을 때

→ 전략적 유연성을 부여하고, 다형성을 극대화하는 데 유리하다.


요약

개념설명
virtual부모 클래스에서 재정의 가능한 메서드 표시
override자식 클래스에서 해당 메서드를 재정의할 때 사용
선택적 오버라이드자식 클래스가 원할 때만 기능을 변경 가능
다형성 활용부모 타입으로 자식 객체를 다루되, 동작은 다르게 구현 가능

가상 메서드는 기본을 제공하되, 자유롭게 바꿀 수 있는 유연한 구조를 만들 때 매우 유용하다.
Unity에서도 MonoBehaviour의 Start(), Update() 등이 사실상 가상 메서드처럼 작동하며,
사용자 정의 클래스에서도 유연한 동작 확장을 위한 핵심 도구로 활용된다.


인터페이스 vs 추상 클래스 vs 가상 메서드 요약

구분인터페이스추상 클래스가상 메서드
구현 여부❌ 구현 없음✅ 일부 구현 가능✅ 기본 구현 있음
필드/속성 사용❌ 불가✅ 가능✅ 가능
상속 개수✅ 다중 가능❌ 하나만 가능(클래스 내 포함 개념)
구현 강제성✅ 전부 구현해야 함✅ abstract만 구현 필요❌ 선택적 override
사용 목적규격 통일 / 강제 구현공통 + 강제 설계공통 제공 + 유연한 확장

처음엔 "다 똑같은 거 아닌가?" 싶지만,

  • "기본 동작을 제공하되 유연하게 바꾸고 싶다" → virtual
  • "공통 코드는 부모가 처리, 특정 기능은 반드시 구현하게 하고 싶다" → abstract class
  • "서로 다른 클래스지만 같은 기능을 강제로 부여하고 싶다" → interface

이렇게 명확히 구분해서 쓰면 된다.

Unity에서 시스템을 짜기 시작할수록 이 구분이 굉장히 중요해진다. 개념이 아니라 설계 감각으로 체득해야 할 듯 싶다.


랜덤 숫자 생성

게임 개발에서 랜덤(Random)은 핵심적인 역할을 한다. 몬스터의 등장 위치, 보상 아이템 드롭, 치명타 발생 확률 등 예측 불가능한 요소를 통해 플레이어에게 다양성과 긴장감을 제공한다. C#에서는 System.Random 클래스를 사용하며, Unity에서는 UnityEngine.Random을 통해 손쉽게 구현할 수 있다.


C#의 Random 클래스 사용법

기본 생성 및 사용
Random rand = new Random();
int number = rand.Next(); // 0 이상 int.MaxValue 이하의 정수
범위 지정하여 생성
int num = rand.Next(1, 101); // 1 이상 100 이하의 랜덤 정수
실수(Random double) 생성
double d = rand.NextDouble(); // 0.0 이상 1.0 미만의 실수

Random.Next()는 정수만 생성 가능, 실수가 필요할 경우 NextDouble()을 사용한다.


Unity에서의 랜덤 숫자 생성

Unity에서는 UnityEngine.Random 클래스를 사용하며, 사용 방식은 직관적이다.

int value = Random.Range(0, 10);       // 0 이상 10 미만 (int)
float value2 = Random.Range(0f, 1f);   // 0.0 이상 1.0 이하 (float)

⚠️ 주의: int최댓값 미포함, float최댓값 포함이다.

이 점을 고려하지 않으면 경계값에서 의도치 않은 버그가 발생할 수 있다.


실전 예시 - 랜덤 위치에 적 생성하기

public GameObject enemyPrefab;

void SpawnEnemy()
{
    float x = Random.Range(-10f, 10f);
    float z = Random.Range(-10f, 10f);
    Vector3 spawnPos = new Vector3(x, 0, z);
    Instantiate(enemyPrefab, spawnPos, Quaternion.identity);
}

Random.Range()를 이용해 적을 맵의 랜덤 위치에 소환할 수 있다. 좌표의 범위를 바꾸면 특정 지역 내 생성도 가능하다.


랜덤 숫자의 게임 내 활용 사례

활용 분야예시 설명
전투 확률 계산치명타 발생 확률, 회피 확률 등 확률 기반 전투 판정
아이템 드랍 시스템확률 기반 드롭 테이블로 아이템 지급
맵 구성타일 배치, 오브젝트 위치 랜덤 설정
AI 의사결정일정 확률로 도망, 공격, 대기 등 행동 패턴 결정
퍼즐 로직매번 다른 숫자 조합 제공 (2048류 게임 등)

기능 요약 정리

기능 구분설명
Random.Next()0 이상 정수 생성
Random.Next(a, b)a 이상 b 미만 정수 생성
Random.NextDouble()0.0 이상 1.0 미만 실수 생성
Random.Range(a, b) (Unity, int)a 이상 b 미만 정수 생성
Random.Range(a, b) (Unity, float)a 이상 b 이하 실수 생성

랜덤은 간단한 기능이지만, 게임의 재미와 변수 설계에 큰 영향을 미친다.
특히 Unity의 Random.Range()는 직관적이면서도 강력한 도구로, 반복 가능한 콘텐츠를 신선하게 만드는 데 핵심적인 역할을 한다.


문자열 처리 (String Manipulation)

문자열 생성 및 길이 확인

string message = "Hello, World!";
int length = message.Length;

문자열 비교

string a = "Hi", b = "hi";
bool equal = a.Equals(b, StringComparison.OrdinalIgnoreCase);

문자열 합치기

string full = a + " " + b;
string formatted = string.Format("{0} {1}", a, b);
string interpolated = $"{a} {b}";

자르기, 나누기, 치환

string s = "apple,banana";
string[] arr = s.Split(',');              // ["apple", "banana"]
string cut = s.Substring(0, 5);           // "apple"
string replaced = s.Replace("apple", "orange");

기타

string trimmed = "  hi  ".Trim();
char[] chars = "hello".ToCharArray();

문자열은 UI, 로그, 대사 시스템, 저장/로드 등 거의 모든 곳에서 쓰인다.


ref / out 키워드

C#에서 refout은 메서드와 호출자 간에 값을 어떻게 공유할 것인지를 결정하는 키워드다.
게임 개발에서는 체력 계산, 무기 효과 반환, 상태 전달 등 다양한 곳에서 사용된다.


ref

  • ref초기화된 값을 메서드에 넘기고, 메서드가 그 값을 직접 수정할 수 있도록 한다.
  • 메서드 안에서 바뀐 값은 호출한 쪽에도 그대로 반영된다.
void AddTen(ref int number)
{
    number += 10;
}

int value = 5;
AddTen(ref value);
Console.WriteLine(value); // 출력: 15

out

  • out초기화하지 않은 변수도 메서드에 넘길 수 있다.
  • 메서드 안에서 반드시 값을 새로 설정해야 하며, 설정된 값이 호출한 쪽으로 전달된다.
void CreateRandom(out int number)
{
    number = new Random().Next(1, 101);
}

int value;
CreateRandom(out value);
Console.WriteLine(value); // 랜덤한 값 출력

ref vs out 비교

항목refout
초기화 여부✅ 메서드 호출 전 반드시 초기화해야 함❌ 초기화 없이 전달 가능
메서드 내 값 설정 필수❌ 선택 (안 해도 됨)✅ 반드시 할당해야 함
대표 용도기존 값 수정, 누적 계산, 상태 전달 등새 값 반환, 랜덤 값 생성, 다중 결과 반환 등

요약

  • ref: 기존 값에 메서드 안에서 변화가 생기고, 그 결과가 밖에서도 반영되어야 할 때
  • out: 메서드 안에서 새로 값을 만들어서 돌려줄 때 유용함

이 두 키워드를 잘 이해하면, return 하나로는 부족했던 데이터를 메서드 바깥으로 간결하게 전달할 수 있고,
Unity에서도 체력 변경, 아이템 생성 결과, 적 중첩 판정 등에 매우 자주 사용된다.


is / as 키워드

C#에서는 객체의 타입을 확인하거나 형변환을 할 때 isas 키워드를 사용한다.
이 둘은 특히 Unity에서 컴포넌트 캐스팅, 이벤트 처리, 다형성 활용 시 매우 자주 등장한다.


is - 타입 검사

  • is는 객체가 특정 타입인지 확인하는 데 사용된다.
  • 결과는 true 또는 false로 반환된다.
object obj = "Hello";
if (obj is string)
{
    Console.WriteLine("문자열입니다");
}

as - 안전한 형변환

  • as는 형변환을 시도하되, 실패하면 예외를 던지지 않고 null을 반환한다.
  • is는 검사만 하지만, as는 실제로 형변환된 객체를 돌려준다.
object obj = "Hello";
string text = obj as string;

if (text != null)
{
    Console.WriteLine("변환 성공: " + text);
}

is vs as 비교

항목isas
기능타입 확인 (bool 반환)안전한 형변환 (실패 시 null)
반환값true / false변환된 객체 or null
예외 발생 여부❌ 없음❌ 없음
대표 용도조건문에서 타입 비교안전한 다운캐스팅

실전 예시 - Unity에서 컴포넌트 캐스팅

void OnTriggerEnter(Collider other)
{
    Enemy enemy = other.GetComponent<Enemy>();

    if (enemy != null)
    {
        enemy.TakeDamage(10);
    }

    // 또는 as 키워드 활용
    IDamageable target = other.GetComponent<IDamageable>() as IDamageable;
    if (target != null)
    {
        target.TakeDamage(10);
    }
}

as는 인터페이스나 상속 기반 구조에서 다운캐스팅할 때 매우 유용하며,
is는 형 확인 후 캐스팅 전에 조건 검사를 할 때 사용된다.


언제 사용해야 할까?

is 키워드가 필요한 경우
  • 객체가 특정 타입인지 확인만 하고 싶을 때
  • 타입에 따라 다른 로직을 실행하고 싶을 때
  • 실제로 캐스팅을 하지 않아도 되는 경우
if (obj is Player) DoSomething();
as 키워드가 필요한 경우
  • 객체를 해당 타입으로 변환해서 사용하고 싶을 때
  • 형변환 실패 시 예외를 피하고 null 처리로 대체하고 싶을 때
  • 주로 인터페이스나 부모 타입으로 받은 객체를 원래의 구체 타입으로 캐스팅할 때
IInteractable interactable = obj as IInteractable;
if (interactable != null) interactable.Use();

요약

  • is: 객체가 특정 타입인지 확인할 때. 조건문에서 주로 사용됨.
  • as: 객체를 안전하게 캐스팅할 때. 실패해도 예외 없이 null 반환.

이 두 키워드를 잘 활용하면 형변환에서 발생할 수 있는 예외를 줄이고, 유연한 타입 설계를 할 수 있다.


이번 학습을 통해 기본 개념부터 객체에 대한 개념까지 C#의 주요 문법과 개념들을 실제 게임 개발에 어떻게 활용할 수 있는지 체계적으로 이해하게 되었다.
단순히 문법을 외우는 것이 아니라, 각각이 어떤 상황에서 쓰이고 왜 중요한지를 맥락 속에서 연결하며 배운 점이 가장 큰 수확이다.
이제 C#이라는 도구를 훨씬 더 유연하고 명확하게 다룰 수 있을 것 같고, Unity 개발을 위한 기초 체력도 훨씬 단단해졌다는 느낌이다.

0개의 댓글