728x90

스택이란?

  • 후입선출(LIFO, Last In First Out) 자료구조
  • 가장 최근에 삽입된 데이터가 가장 먼저 삭제

1. 스택의 기본 구조

스택의 기본 연산(push, pop, top, isEmpty)을 설명하는 예제

  • push : 스택에 요소를 추가, 배열의 끝에 새 요소르 삽입하고, top 인덱스를 증가시킴
  • pop : 스택에서 요소를 제거, 배열의 끝에서 요소를 제거하고, top 인덱스를 감소
  • peek : 스택의 최상위 요소를 확인, 스택의 top 인덱스에 있는 요소를 반환
  • isEmpty : 스택이 비어 있는지 확인, top 인덱스가 -1이면 스택이 비어있는 것

스택 구현

#include <iostream>
#define MAX 1000 // 스택의 최대 크기

using namespace std;

class Stack
{
    int top;
    int capacity;        // 스택의 크기

public:
    int a[MAX];            // 스택 배열

    Stack() 
    {
        top = -1;         // 스택 초기화
        capacity = 0;    // 스택의 크기 초기화
    }

    bool push(int x);
    int pop();
    int peek();
    bool isEmpty();
    int size();            // 스택의 크기를 반환하는 함수
};

// 스택에 요소를 추가하는 함수
bool Stack::push(int x)
{
    if(top >= (MAX - 1))
    {
        cout<< "스택 오버플로우\n";
        return false;
    }
    else
    {
        a[++top] = x;
        capacity++;
        cout<<x<<" 스택에 푸쉬 되었습니다\n";
        return true;
    }
}

// 스택에서 요소를 제거하는 함수
int Stack::pop()
{
    if(top < 0)
    {
        cout<< "스택 언더플로우\n";
        return 0;
    }
    else
    {
        int x = a[top--];
        capacity--;
        return x;
    }
}


// 스택에 최상위 요소를 반환하는 함수
int Stack::peek()
{
    if(top < 0)
    {
        cout<<"스택에 쌓인게 없어요\n";
        return 0;
    }
    else
    {
        int x = a[top];
        return x;
    }
}

// 스택이 비었는지 확인하는 함수
bool Stack::isEmpty()
{
    return (top<0);
}

// 스택의 크기를 반환하는 함수
int Stack::size()
{
    return capacity;
}


int main()
{
    Stack s;
    s.push(10);
    s.push(20);
    s.push(30);
    cout<<s.pop()<<"\n";
    cout<<s.peek()<<"\n";
    cout<<s.size()<<"\n";
    cout<<s.isEmpty()<<"\n";

    return 0;
}

과제로 하면 좋을 것

  1. 스택의 크기 반환 함수를 추가해보자
  2. 스택을 동적 배열로 구현해보자
  3. 연결 리스트를 이용한 스택을 구현해보자
  4. 예외 처리를 추가하여 더 견고한 스택 클래스를 만들어보자

필요에 따라 STL의 std::stack을 사용하는 것도 좋은 방법

스택을 동적 배열로 구현

#include <iostream>
#include <vector>

using namespace std;

class Stack {
private:
    vector<int> a; // 동적 배열을 이용한 스택

public:
    Stack() {}

    bool push(int x);
    int pop();
    int peek();
    bool isEmpty();
    int size(); // 스택의 크기를 반환하는 함수
};

// 스택에 요소를 추가하는 함수
bool Stack::push(int x) {
    a.push_back(x);
    cout << x << " 스택에 푸쉬 되었습니다\n";
    return true;
}

// 스택에서 요소를 제거하는 함수
int Stack::pop() {
    if (a.empty()) {
        cout << "스택 언더플로우\n";
        return 0;
    } else {
        int x = a.back();
        a.pop_back();
        return x;
    }
}

// 스택의 최상위 요소를 반환하는 함수
int Stack::peek() {
    if (a.empty()) {
        cout << "스택에 쌓인게 없어요\n";
        return 0;
    } else {
        return a.back();
    }
}

// 스택이 비었는지 확인하는 함수
bool Stack::isEmpty() {
    return a.empty();
}

// 스택의 크기를 반환하는 함수
int Stack::size() {
    return a.size();
}

int main() {
    Stack s;
    s.push(10);
    s.push(20);
    s.push(30);
    cout << s.pop() << " 스택에서 팝 되었습니다\n";
    cout << "최상위 요소는 " << s.peek() << " 입니다\n";
    cout << "스택의 크기는 " << s.size() << " 입니다\n";
    cout << "스택이 비었나요? " << (s.isEmpty() ? "네" : "아니요") << endl;

    return 0;
}
728x90
728x90

마크다운이란?

  • 텍스트 기반의 마크업 언어
  • 간단한 문법을 사용하여 HTML로 변환할 수 있음
  • 쉽게 포맷팅된 텍스트를 작성할 수 있음
    • 티스토리나 깃허브에서도 마크다운을 사용하여 글을 작성할 수 있음

기본 문법

줄바꿈(Line Breaks)

  1. 두칸의 공백을 사용한 줄바꿈
    줄 끝에 두 칸의 공백을 추가한 후 줄바꿈을 하면 됨
첫 번째 줄 끝에 두 칸의 공백을 추가합니다.  
그리고 다음 줄을 작성합니다.
  1. HTML 태그를 사용한 줄바꿈

    태그를 사용하여 줄바꿈을 할 수 있음

    첫 번째 줄 끝에 `<br>` 태그를 추가합니다.<br>
    그리고 다음 줄을 작성합니다.
  2. 여러 줄 공백(Multiple Line Breaks)
    마크다운 자체는 여러 줄 공백을 기본적으로 지원하지 않지만, HTML 태그를 사용하여 해결할 수 있음

     &nbsp; - Non-Breaking Space        // 연속된 공백을 유지하고 싶을 떄 사용
     &ensp; - En Space                // 일반적인 공백보다 약간 넓은 공백(보통 문자'n'의 너비)
     &emsp; - Em Space                // 일반적인 공백보다 더 넓은 공백(보통 문자 ‘m’의 너비).
     &thinsp; - Thin Space            // 매우 좁은 공백
     &hairsp; - Hair Space            // 거의 눈에 띄지 않는 매우 좁은 공백
     &zwnj; - Zero Width Non-Joiner    // 너비가 없는 공백. 문자들이 결합되지 않도록 함
     &zwj; - Zero Width Joiner        // 너비가 없는 공백. 문자들을 결합시킴
     &lrm; - Left-To-Right Mark        // 방향을 왼쪽에서 오른쪽으로 설정하는 공백
     &rlm; - Right-To-Left Mark        //방향을 오른쪽에서 왼쪽으로 설정하는 공백

제목(Header)

제목은 #문자를 사용하여 작성

#의 개수에 따라 제목의 레벨이 결정됨

# 제목 1
## 제목 2
### 제목 3
#### 제목 4
##### 제목 5
###### 제목 6

**

목록(List)**

1. 순서 있는 목록(Ordered List)

숫자와 점을 사용하여 작성

1. 첫 번쨰 항목
2. 두 번쨰 항목
3. 세 번쨰 항목

2. 순서 없는 목록(Unordered List)

하이픈(-), 별표(*), 또는 더하기 기호(+)를 사용하여 작성

- 첫 번째 항목
- 두 번째 항목
- 세 번째 항목

* 첫 번째 항목
* 두 번째 항목
* 세 번째 항목

+ 첫 번째 항목
+ 두 번째 항목
+ 세 번째 항목

**

강조(Emphasis)**

  1. 기울임(Italic)

별표(*)나 언더스코어(_) 한 개로 감싼다.

*기울임*
_기울임_
  1. 굵게(Bold)

별표(*)나 언더스코어(_) 두 개로 감싼다.

**굵게**
__굵게__
  1. 취소선(Strikethrough)

물결표(~) 두 개로 감싼다.

~~취소선~~

**

링크(Links)**

대괄호로 링크 텍스트를 감싸고, 뒤에 괄호로 링크 URL을 작성

[Google](https://www.google.com)

이미지(Images)

느낌표(!)를 앞에 추가하고 링크와 같은 형식으로 작성

![Alt text](이미지 URL)

**

코드(Code)**

  1. 인라인 코드(Inline Code)

백틱(```)으로 감싼다.

`코드`
  1. 코드 블록(Code Block)

백틱 세 개로 감쌉니다. 언어를 명시하면 문법 강조(Syntax Highlighting)가 적용

```python
print("Hello, World!")

인용문(Blockquote)

> 기호를 사용하여 작성합니다.

> 이것은 인용문입니다.
> 여러 줄에 걸쳐 작성할 수 있습니다

수평선(Horizontal Rule)

하이픈(-), 별표(*), 또는 언더스코어(_) 세 개를 사용합니다.

---
***
___
728x90
728x90

코루틴이란?

  • 특정 작업을 여러 프레임에 걸쳐 나누어 수행
  • 특정 조건이 충족될 때 까지 기다리면서 코드 실행을 중단할 수 있도록 하는 강력한 기능
  • 게임에서 비동기 작업을 처리하는 데 사용
    • 시간 지연
    • 애니메이션
    • 비동기 입력 처리 등...

Update 함수와 코루틴의 차이점

Update 함수

  • 매 프레임 호출
  • 해당 프레임 내에서 모든 작업을 완료해야 함
  • 작업이 길어질 경우 프레임 드랍이나 렉이 발생할 수 있음.

 
이 문제를 해결하기 위해 Unity에서는 시분할(time sharing)방식의 Coroutine 사용
 
코루틴을 사용하면 긴 작업을 여러 프레임에 걸쳐 분할하여 수행가능
 
작업이 완료될 때까지 기다리지 않고 프레임이 부드럽게 유지

코루틴 사용법

1. 코루틴 정의

  • 코루틴은 IEnumerator 타입을 반환하는 메서드로 정의
  • IEnumerator는 코루틴의 상태를 유지하고, 다음 단계로 진행하기 위해 호출될 때마다 상태를 복원
using UnityEngine;
using System.Collections;

public class CoroutineExample : MonoBehaviour
{
    void Start()
    {
        // 코루틴 시작
        StartCoroutine(MyCoroutine());
    }

    IEnumerator MyCoroutine()
    {
        // 코루틴 시작 시 출력
        Debug.Log("Coroutine started");

        // 1초 기다리기
        yield return new WaitForSeconds(1.0f);

        // 1초 후 출력
        Debug.Log("1 second passed");

        // 프레임 끝까지 기다리기
        yield return null;

        // 다음 프레임에서 출력
        Debug.Log("Next frame");

        // 코루틴 종료
        yield break;
    }
}



2. 코루틴 시작

  • 코루틴은 StartCoroutine 메서드를 사용하여 시작
  • 코루틴을 시작할 때는 코루틴 메서드의 이름을 전달하거나 IEnumerator를 반환하는 메서드를 호출할 수 있음.
StartCoroutine(MyCoroutine());



3. 코루틴 중단

  • 코루틴을 중단하려면 StopCoroutine 메서드를 사용
  • 특정 코루틴을 중단하려면 그 코루틴의 참조를 전달해야 함.
Coroutine myCoroutine;

void Start()
{
    myCoroutine = StartCoroutine(MyCoroutine());
}

void StopMyCoroutine()
{
    if (myCoroutine != null)
    {
        StopCoroutine(myCoroutine);
    }
}



4. yield return 구문

yield return null; // 다음 프레임까지 대기
yield return new WaitForSeconds(float seconds); // 지정한 시간(초) 동안 대기
yield return new WaitForEndOfFrame(); // 모든 렌더링이 완료될 때까지 대기
yield return new WaitForFixedUpdate(); // 다음 고정 업데이트(physics step)까지 대기
yield return StartCoroutine(AnotherCoroutine()); // 다른 코루틴이 완료될 때까지 대기

 

코루틴 사용 예제

using UnityEngine;
using System.Collections;

public class EnemyAI : MonoBehaviour
{
	// 적 캐릭터가 3초 동안 대기한 후 플레이어를 추격하는 예제
    
    public Transform player;
    public float chaseSpeed = 5.0f;

    void Start()
    {
        StartCoroutine(ChasePlayerAfterDelay(3.0f));
    }

    IEnumerator ChasePlayerAfterDelay(float delay)
    {
        // 지연 시간 동안 대기
        yield return new WaitForSeconds(delay);

        // 추격 시작
        while (true)
        {
            // 플레이어 방향으로 이동
            transform.position = Vector3.MoveTowards(transform.position, player.position, chaseSpeed * Time.deltaTime);
            yield return null;
        }
    }
}

 

 

참고 문헌 : https://docs.unity3d.com/kr/2021.3/Manual/Coroutines.html

728x90

'게임엔진 > Unity' 카테고리의 다른 글

Unity - OnMouseDown  (0) 2024.07.02
Unity - GetAxis  (0) 2024.07.02
Unity - 선형 보간  (0) 2024.07.02
유니티 게임 프로그래밍 패턴 - SOLID 원칙  (0) 2024.06.19
유니티 - InspectorAttribute  (0) 2024.05.03
728x90

static_cast

  • static_cast는 C++에서 타입 변환을 수행할 때 사용하는 캐스트 연산자이다.
  • static_cast는 컴파일 시간에 타입 변환을 검사하며, 안전한 변환만을 허용한다.

 

기본 문법

static_cast<new_type>(expression)
// new_type : 변환하고자 하는 타입
// expression : 변환 대상이 되는 표현식

 

사용예제

// 기본 타입 변환
int a = 10;
double b = static_cast<double>(a) // int를 double로 변환

// 포인터 타입 변환
class Base {};
class Derived : public Base {};

Base* base new Derived();
Derived* derived = static_cast<Derived*>(base); // 안전한 다운캐스팅

// 열거형 타입 변환
eunm class Colors { Red, Green, Blue };
int colorValue = static_cast<int>(Colors::Red); // 열거형을 정수형으로 변환

 

주의사항

  • static_cast는 컴파일 시간에 타입 검사만 하므로, 런타임에 안전성을 보장하지는 않음.
    예를 들어, 상속 관계에서 기본 클래스 포인터를 유도 클래스 포인터로 변환할 때, 실제로 유도 클래스 객체를 가리키고 있지 않으면 잘못된 결과를 초래할 수 있음.
  • 잘못된 타입 변환을 시도할 경우, 컴파일 오류가 발생할 수 있음.

 

728x90

'전산 > C++' 카테고리의 다른 글

c++ - std::endl 과 "\n"의 차이  (0) 2024.06.29
728x90

백준 하노이 탑 이동 순서 문제

endl를 "\n"로 바꿨을 뿐인데 맞았다.

eldl를 썼을 때
\n를 썼을 때



endl과 "\n" 은 C++에서 개행을 나타내는 방법이다.

 

 

endl은 C++ 표준 라이브러리 에 정의된 출력 스트림 조직자 이다.

  • 출력 스트림에 개행 문자("\n")을 출력한다.
  • 버퍼를 비워서 (파일 출력의 경우 파일에 쓰기를 강제로 실행함으로써) 출력이 즉시 화면에 나타난다.
  • 일반적으로 버퍼링을 사용하는 출력에서는 endl을 사용하여 출력을 하면 비효율적이 될 수 있다.

버퍼 : 일시적으로 데이터를 저장하는 메모리 공간

버퍼링 : 데이터를 일시적으로 버퍼에 저장하여 입출력 성능을 향상시키는 기술

 

"\n"은 개행 문자로 문자 리터럴로 사용된다.

  • 단순히 문자열에 개행 문자를 추가한다.
  • 이는 버퍼를 비우지 않고, 다음 출력이 이어서 나타낼 수 있다.

 

 

 

결론 : 일반적으로 개행만 필요하다면 "\n"을 사용하는 것이 효율적이다.

endl은 특정 상황에서 버퍼를 비우는 것이 필요할 때 사용하자.

728x90

'전산 > C++' 카테고리의 다른 글

C++ - static_cast  (0) 2024.07.01
728x90
brew update
// Homebrew 패키지 관리자의 패키지들을 최신 상태로 업데이트
// Homebrew는 macOS에서 패키지 관리를 쉽게 할 수 있도록 도와주는 도구

brew install mysql
// Homebrew를 사용하여 MYSQL 데이터베이스 서버를 설치

mysql --version
// 설치된 MYSQL의 버전을 확인

mysql.server start
// MYSQL 서버를 시작 Homebrew를 통해 설치한 경우 mysql.server 명령을 사용하여 MySQL을 시작할 수 있음

mysql_secure_installation
// MYSQL을 초기 설정하는 명령 
// 이 과정에서 MYSQL root 사용자의 비밀번호 설정 및 보안 관련 설정을 수행할 수 있음

//Enter current password for root (enter for none): [Press Enter]
//(초기 설정이므로 비밀번호가 없다면 그냥 엔터를 누릅니다)
//Set root password? [Y/n] Y
//새로운 root 비밀번호를 설정합니다.
//Remove anonymous users? [Y/n] Y
//익명 사용자를 삭제할지 여부를 물어봅니다.
//Disallow root login remotely? [Y/n] Y
//원격에서 root 계정으로 로그인을 허용할지 여부를 물어봅니다.
//Remove test database and access to it? [Y/n] Y
//테스트 데이터베이스를 삭제할지 여부를 물어봅니다.
//Reload privilege tables now? [Y/n] Y
//권한 테이블을 다시 불러올지 여부를 물어봅니다.

mysql -u root -p
// MYSQL에 root 사용자로 접속하는 명령
// 이 명령을 통해 MYSQL 콘솔에 접근할 수 있음.

//-u root: root 사용자로 접속합니다.
//-p: 비밀번호를 입력하라는 프롬프트가 나타납니다. 설정한 root 비밀번호를 입력하세요.

 

SHOW DATABASES;
// MYSQL 서버에 존재하는 모든 데이터베이스의 목록을 조회하는 명령어
// 모든 데이터베이스의 이름이 나열

CREATE DATABASE (데이터베이스 이름);
// 새로운 데이터베이스를 생성하는 명령어
// 데이터베이스 이름을 지정하여 새로운 데이터베이스를 생성할 수 있음

USE (데이터베이스 이름);
// 지정된 데이터베이스를 사용하도록 설정하는 명령어
// 해당 데이터베이스 내에서 쿼리를 실행할 수 있음

CREATE TABLE (테이블 이름)
// 새로운 테이블을 생성하는 명령어
// 이 명령어를 사용하여 데이터베이스 내에 테이블을 만들고
// 각 열의 이름 데이터 유형, 제역조건  등을 지정할 수 있음

CREATE TABLE employees (
    id INT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    position VARCHAR(50),
    salary DECIMAL(10, 2) DEFAULT 0.00
);

INSERT INTO (테이블 이름)
// 새로운 데이터를 삽입하는 명령어
// 특정 테이블에 새로운 행을 추가할 수 있음

INSERT INTO employees (id,name,position,salary) VALUES(1,'WooMin','Manager',75000);

exit
// MYSQL 콘솔에서 나가는 명령어
// 현재 MYSQL 콘솔 세션을 종료하고 쉘 또는 터미널로 돌아감

mysql.server stop
// macOS에서 MYSQL 서버를 정지하는 명령어
// Homevrew를 통해 설치한 MYSQL을 관리할 때 사용할 수 있음​

 

 

 

728x90
728x90
증명 proof 공리 axiom
정의 definition 정리 theorem
직접 증명법 direct proof 소수 prime number
모순 증명법 proof by contradiction 대우 증명법 proof by contrapositive
존재 증명법 existence proof 반례 증명법 proof by counter-example
수학적 귀납법 mathematical induction 완전수 perfect number
완전 제곱수 perfect square number    

 

728x90
728x90
진릿값 truth value true
거짓 false 명제 proposition
단순명제 simple proposition 활성명제 compound proposition
진리표 truth table 부정 NOT
논리곱 AND (conjunction) 논리합 OR
배타적 논리합  Exclusive OR, XOR 동치 equivalence
조건명제 conditional proposition 함축 implication
전제1 1. premise (2.hypothesis) 가정 hypothesis
결론 conclusion 결과 consequence
converse inverse
대우 contraposition 쌍방조건명제 biconditional proposition
항진명제 tautology 모순명제 contradiction
사건명제 contingency 논리적 동치 logically equivalence
항등법칙 identity law 지배법칙 domination law
부정법칙 negation law 이중 부정법칙 double negation law
멱등법칙 idempotent law 교환법칙 commutative law
결합법칙 associative law 분배법칙 distributive law
드 모르간의 법칙 De Morgan's law 흡수법칙 absorption law
함축법칙 implication law 명제함수 propositional function
논의영역 domain of discource 구속변수 binding variable
자유변수 free variable 전체 한정자 universal quantifier
존재 한정자 existential quantifier 추론 inference
논증 reasoning 유효추론 / 정당한 추론 valid inference
허위추론 / 부당한 추론 fallacious inference 선언적 부가 disjunctive addition
단순화 simplication 긍정논법 modus ponens
부정논법 modus tollens 선언적 삼단논법 또는 소거 disjunctive syllogism
가설적 삼단논법 또는 추이 hypothetical syllogism    
728x90
728x90
자연수 nutural number 정수 integer
유리수 rational number 무리수 irrational number
실수 real number 허수 단위 imaginary unit
복소수 complex number 허수 imaginary number
항등원 identity element 역원 inverse element
합연산 시그마(sigma) 곱연산 프로덕트(product)
계승 팩토리얼(factorial) quotient
나머지 remainder 몫을 구하는 연산 div
나머지를 구하는 연산 mod 10진법 decimal number system
10진수 decimal number 2진법 binary number system
2진수  binary number 8진법 octal number system
8진수 octal number 16진수 hexadecimal number system
16진수 hexadecimal number 비트 bit,binary digit
보수 complement 1의 보수 1's complement
2의 보수 2's complement 부호-절댓값표현 sign-magnitude
부호-1의 보수 sign-1's complement 부호-2의보수 sign-2's complement
초과 overflow 기수 base

 

728x90
728x90

SOLID 원칙

SOLID는 소프트웨어 디자인의 다섯 가지 핵심 원칙을 머리글자어로 만든 용어

  • 단일 책임(Single responsibility)
  • 개방 폐쇄(Open closed)
  • 리스코프 치환(Liskov substiution)
  • 인터페이스 분리(Interface segregation)
  • 종속성 역전(Dependency inversion)

 

 

 

단일 책임 원칙(SRP)

클래스를 변경해야 한다면 그 이유는 오직 단일 책임 원칙이어야 한다.

  • 모듈, 클래스 또는 함수가 오직 한 가지만 책임지며 로직의 특정 부분만 캡슐화할 것을 명시
  • 단일 구조의(monolithic) 클래스를 만들기보다는 여러 개의 작은 클래스로 프로젝트를 조합
  • 클래스와 메서드는 짧을수록 설명과 이해, 구현이 쉬움유니티 예시
  • 3D 모델에 대한 참조가 있는 MeshFilter
  • 모델 표면이 화면에 표시되는 방식을 제어하는 Renderer
  • 스케일, 회전, 위치 정보가 있는 Transform 컴포넌트
  • 물리 시뮬레이션과 상호 작용하는 데 필요한 Rigidbody
public class UnrefactoredPlayer : MonoBehaviour
{
    [SerializeField] private string inputAxisName;
    [SerializeField] private float positionMultiplier;
    private float yPosition;
    private AudioSource bounceSfx;
    private void Start()
    {
        bounceSfx = GetComponent<AudioSource>();
    }
    private void Update()
    {
        float delta = Input.GetAxis(inputAxisName) * Time.deltaTime;
        yPosition = Mathf.Clamp(yPosition + delta, -1, 1);
        transform.position = new Vector3(transform.position.x,
yPosition * positionMultiplier, transform.position.z);
}
    private void OnTriggerEnter(Collider other)
    {
        bounceSfx.Play();
    }
}

위 코드에서 UnrefactoredPlayer 클래스는 너무 많은 기능을 수행한다.
플레이어가 무언가에 충돌하면 소리를 재생하고, 입력을 관리하고, 이동을 처리 지금은 비교적 짧은 클래스지만
프로젝트를 진행하다 보면 점점 유지하기 어려워질 것 이다.
Player 클래스를 더 작은 여러 클래스로 분할해보자.

 

 

 

[RequireComponent(typeof(PlayerAudio), typeof(PlayerInput),
typeof(PlayerMovement))]
public class Player : MonoBehaviour
{
    [SerializeField] private PlayerAudio playerAudio;
    [SerializeField] private PlayerInput playerInput;
    [SerializeField] private PlayerMovement playerMovement;
    private void Start()
    {
        playerAudio = GetComponent<PlayerAudio>();
        playerInput = GetComponent<PlayerInput>();
        playerMovement = GetComponent<PlayerMovement>();
} }
public class PlayerAudio : MonoBehaviour
{
... }
public class PlayerInput : MonoBehaviour
{
... }
public class PlayerMovement : MonoBehaviour
{
...
}

Player 스크립트가 여전히 스크립팅된 다른 컴포넌트를 관리할 수 있지만, 각 클래스는 오직 한 가지 역할만 수행한다.
이렇게 디자인하면 코드를 더 쉽게 수정할 수 있으며, 특히 시각이 지나며 프로젝트 요구 사항이 바뀌는 상황에서는 더욱 유용하다.
하지만 단일 책임 원칙이라도 합당한 상식선에서 적용해야 한다.
하나의 메서드만으로 클래스를 만드는 극단적인 간소화는 피해야 한다.

 

 

 

 

단임 책임 원칙을 따라 작업할 때 염두에 둘 만한 목표

  • 가독성
    • 클래스가 짧으면 읽기 쉬움
    • 엄격하고 직관적인 규칙은 없지만 많은 개발자가 라인의 수를 200~300개 정도로 제한
    • 본인 또는 팀 차원에서 어느 정도를 짧다고 규정할지 원칙을 정해야 함
    • 정해진 한도를 초과하면 더 작게 리팩터링할 것 인지 결정
  • 확장성
    • 작은 클래스에서 상속하기가 더 쉬움
    • 의도치 않은 기능 장애를 걱정할 필요 없이 클래스를 수정하거나 대체
  • 재사용성
    • 게임의 다른 부분에 재사용할 수 있도록 클래스를 작은 모듈형으로 디자인

리팩터링할 때는 코드를 어떻게 재구성해야 자신과 팀원에게 도움이 될지 생각하자.
초반에 약간의 노력을 더 들이면 많은 문제를 미연에 방지할 수 있다.

 

 

 

 

단순함의 중요성

단순성은 소프트웨어 디자인에서 자주 다루는 주제이며 신뢰성을 높이기 위한 전제 조건이기도 하다.
소프트웨어가 제작 단계에서 변경이 이뤄져도 대응할 수 있도록 디자인되었나?
앞으로 애플리케이션을 확장하고 유지 관리할 수 있나?

디자인 패턴과 원칙으로 코드의 확장성, 유연성, 가독성을 높일 수 있다.
하지만 추가 작업과 계획이 필요하다.
"단순함"과 "쉬움"은 동의어가 아니다.

패턴을 사용하지 않아도 같은 기능을 더 빠르게 만들 수 있지만, 빠르고 쉬운 작업이 반드시 단순한 결과물로 이어지지는 않는다.
단순하게 만들면 결과물의 집중도가 높아진다.
하나의 작업만 수행하도록 디자인하고, 다른 작업으로 지나치게 복잡도를 높이지 말자.

 

 

 

 

 

개방 폐쇄 원칙(OCP)

클래스가 확장에는 개방적이되 수정에는 폐쇄적이어야 한다고 명시되어 있다.
원본 코드를 수정하지 않고도 새로운 동작을 생성할 수 있도록 클래스를 구조화 하자.

public class AreaCalculator
{
    public float GetRectangleArea(Rectangle rectangle)
    {
        return rectangle.width * rectangle.height;
    }
    public float GetCircleArea(Circle circle)
    {
        return circle.radius * circle.radius * Mathf.PI;
    }
}
public class Rectangle
{
     public float width;
     public float height;
}
public class Circle
{
    public float radius;
}

위 코드도 충분히 기능을 수행하지만, AreaCalculator에 더 많은 세이프를 추가하려면
각각의 새로운 셰이프를 위한 메서드를 생성해야 할 것이다.
나중에 여기서 오각형이나 팔각형을 보내야 한다면 어떻게 해야 하나?
셰이프를 20개 더 추가해야 한다면 어떠할까?
AreaCalculator 클래스는 지나치게 커져 통제 불능 상태에 빠질 것 이다.

Shape라는 기본 클래스를 만들고 셰이프를 처리할 메서드를 하나 만들 수도 있다.

하지만 그렇게 하려면 각 셰이프 형식을 처리하도록 로직 안에 여러 개의 if문이 있어야 한다.

그렇게 구현하면 확장성이 떨어진다.

원본 코드(AreaCalculator의 내부)를 수정하지 않고, 새로운 셰이프를 사용할 수 있도록 확장을 위한 프로그램을 여는 것이 좋다.

현재 AreaCalculator는 작동하지만, 개방 폐쇄 원칙을 위반한다.

다음과 같이 추상 Shape 클래스를 정의 해 보자.

 

 

 

public abstract class Shape
{
    public abstract float CalculateArea();
}

위 코드는 CalculateArea라는 이름의 추상 메서드가 있다.
이제 Rectangle과 Circle이 Shape로부터 상속하도록 하면, 각 셰이프는 각자의 영역을 계산하고 다음과 같은 결과를 반환할 수 있다.

 

 

 

 

public class Rectangle : Shape
{
    public float width;
    public float height;
    public override float CalculateArea()
    {
        return width * height;
    }
}
public class Circle : Shape
{
    public float radius;
    public override float CalculateArea()
    {
        return radius * radius * Mathf.PI;
    }
}

AreaCalculator를 단순화할 수 있다.

 

 

 

public class AreaCalculator
{
    public float GetArea(Shape shape)
    {
        return shape.CalculateArea();
    }
}

수정된 AreaCalculator 클래스는 추상 Shape 클래스를 적절히 구현하는 셰이프의 영역을 가져올 수 있다.
원본 소스를 전혀 변경하지 않고 AreaCalculator 기능을 확장할 수 있다.

새로운 다각형이 필요할 때마다 Shape에서 상속하는 새 클래스를 정하면 된다.
각각의 서브 클래스 셰이프는 CalculateArea 메서드를 오버라이드하여 올바른 영역을 반환한다.

이 새로운 디자인을 사용하면 디버깅하기도 더 쉽다.

새로운 셰이프로 오류가 발생하더라도 AreaCalculator를 재검토할 필요가 없다.

기존코드가 변경없이 유지되므로, 새 코드에만 잘못된 로직이 있는지 조사하면 된다.

 

 

 

 

 

새로운 클래스를 만들 때 인터페이스와 추상화를 활용하자

나중에 확장하기 까다로운 switch 또는 if문을 로직에 넣지 않아도 된다.
OCP에 맞춰 클래스를 설정하는 데 익숙해지면 장기적으로 새로운 코드를 더 간편하게 추가할 수 있게 된다.

 

 

 

 

 

리스코프 치환 원칙(LSP)

파생된 클래스가 기본 클래스로 대체될 수 있어야 한다고 명시한다.
객체 지향 프로그래밍에서 상속을 사용하면 서브 클래스를 통해 기능을 추가할 수 있다.
하지만 주의하지 않으면 불필요한 복잡성을 더할 수 있다.

  • 상속을 적용하여 서브 클래스를 더 강력하고 유연하게 만드는 방법
public class Vehicle
{
    public float speed = 100;
    public Vector3 direction;
    public void GoForward()
    {
    ...
    }
    public void Reverse()
    {
    ...
    }
    public void TurnRight()
    {
    ...
     }
    public void TurnLeft()
    {
    ...
     }
}

보드에서 차량을 옮기는 턴제 게임을 개발한다고 가정
사전에 정해진 경로에 따라 차량을 운전하는 Navigator라는 클래스를 추가할 수 있다.

 

 

 

public class Navigator
{
    public void Move(Vehicle vehicle)
    {
        vehicle.GoForward();
        vehicle.TurnLeft();
        vehicle.GoForward();
        vehicle.TurnRight();
        vehicle.GoForward();
    } 
}

이 클래스를 사용하면 어떤 차량이든 Navigator 클래스의 Move 메서드로 전할 수 있으며,
자동차와 트럭에는 이 방법이 잘 통할 것이다.
하지만 Train이라는 클래스를 구현하려는 경우 어떻게 해야 할까?

기차는 철도를 이탈할 수 없으므로 TurnLeft와 TurnRight 메서드는 Train 클래스에서 작동하지 않는다.

Navigator의 Move 메서드로 기차를 전달하는 경우, 해당 라인에 도달하면 구현되지 않은 예외가 발생하거나 아무 일도 일어나지 않을 것 이다.

특정 유형을 하위 유형과 교체할 수 없다면 리스코프 치환 원칙을 위반하게 된다.

Train은 Vehicle의 하위 유형이므로 Vehicle 클래스가 허용되는 모든 위치에 사용할 수 있어야 한다.

그러지 않으면 코드가 예측할 수 없는 방식으로 작동할 수 있다.

 

 

 

 

리스코프 치환 원칙을 더 철저하게 준수하기 위한 몇 가지 팁

  • 서브 클래스를 설정할 때 기능을 제거하면 리스코프 치환을 위반하게 될 가능성이 크다.
    • NotImplementedException은 이 원칙을 위반했다는 의미이며, 메서드를 비워두는 경우도 마찬가지이다.
    • 서브 클래스가 기본 클래스처럼 동작하지 않는다면 오류나 예외가 명시적으로 보이지 않더라도 LSP를 준수하지 않는 것이다.\
  • 추상화를 단순하게 유지 하자
    • 기본 클래스에 들어가는 로직이 많을수록 LSP를 위반할 확률도 커진다.
    • 기본 클래슨느 파생 서브 클래스의 일반적인 기능만 표현해야 한다.
  • 클래스 계층 구조를 수립할 때 클래스 API를 고려한다.
    • 대상을 모두 차량으로 간주하더라도 Car와 Rrain은 각각 서로 다른 부모 클래스로부터 상속하는 편이 더 나을 수도 있다.
    • 실질적으로 분류가 항상 클래스 계층 구조와 일치하지는 않는다.
  • 상속보다는 합성을 우선시하자
    • 상속을 통해 기능의 전달을 시도하는 대신, 특정한 동작을 캡슐화할 있도록 인터페이스 또는 별도의 클래스를 만들자.
    • 그런 다음 믹스 앤 매치를 통해 다양한 기능의 합성물을 생성한다.

 

 

 

이 디자인을 수정하려면 원본 Vehicle 유형을 삭제한 다음 대부분의 기능을 인터페이스로 옮긴다.

public interface ITurnable
{
    public void TurnRight();
    public void TurnLeft();
}
public interface IMovable
{
    public void GoForward();
    public void Reverse();
}

RoadVehicle 유형과 RailVehicle 유형을 만들면 LSP 원칙을 더 철저하게 따를 수 있다.
Car 와 Train은 해당하는 기본 클래스로 부터 상속한다.

 

 

public class RoadVehicle : IMovable, ITurnable
{
    public float speed = 100f;
    public float turnSpeed = 5f;
    public virtual void GoForward()
    {
    ... 
    }
    public virtual void Reverse()
    {
    ... 
    }
    public virtual void TurnLeft()
    {
    ... 
    }
    public virtual void TurnRight()
    {
    ... 
    }
}
public class RailVehicle : IMovable
{
    public float speed = 100;
    public virtual void GoForward()
    {
    ... 
    }
    public virtual void Reverse()
    {
    ... 
    }
}
public class Car : RoadVehicle
{
... 
}
public class Train : RailVehicle
{
... 
}

이러한 방법에서는 기능이 상속 대신 인터페이스를 통해 실행된다.
Car와 Train 클래스가 더 이상 같은 기본 클래스를 공유하지 않으며,
이는 LSP를 준수한다.

같은 기본 클래스에서 RoadVehicle과 RailVehicle을 파생시킬 수도 있으나 이 경우에는 크게 필요하지 않는다.

이러한 사고방식은 직관적이지 않은 것처럼 보일 수 있는데, 사람들이 실제 세상에 대해 가지는 확고한 가정이 있기 때문이다.
소프트웨어 개발에서는 이를 원 타원 문제(Circle ellipseproblem)라고 한다.
모든 실제 등가 관계가 상속으로 전환되지는 않는다.

소프트웨어 디자인으로 진행하려는 것은 실제 세상에 대한 사전 지식이 아닌, 클래스 계층 구조라는 사실을 기억하자.

리스코프 치환 원칙에 따라 상속 사용 방법에 제한을 두어 코드 베이스를 확장 가능하고 유연하게 유지해야 한다.

 

 

 

 

 

인터페이스 분리 원칙(ISP)

어떠한 클라이언트도 자신이 사용하지 않는 메서드에 갖엦로 종속될 수 없다고 명시되어 있다.

  • 인터페이스의 규모가 커지지 않도록 해야 한다.
  • 클래스와 메서드의 길이를 짧게 유지하라는 단일 책임 원칙과 같은 맥락으로 이해하자
  • 유연성을 최대로 향상하며, 집중도가 높고 컴팩트한 인터페이스를 유지할 수 있다.

다양한 플레이어 유닛이 있는 전략 게임을 만든다고 상상해보자.
각 유닛에는 체력과 속도를 비롯한 다양한 스탯이 있다.
다음과 같이 모든 유닛이 유사한 기능을 구현하도록 보장하는 인터페이스를 만들자

public interface IUnitStats
{
    public float Health { get; set; }
    public int Defense { get; set; }
    public void Die();
    public void TakeDamage();
    public void RestoreHealth();
    public float MoveSpeed { get; set; }
    public float Acceleration { get; set; }
    public void GoForward();
    public void Reverse();
    public void TurnLeft();
    public void TurnRight();
    public int Strength { get; set; }
    public int Dexterity { get; set; }
    public int Endurance { get; set; }
}

부술 수 있는 통이나 상자 등 파괴 가능한 프랍을 만든다고 가정해보자
비록 움직이지 않는 프랍이지만 여기에도 체력이라는 개념이 필요하다.
또한 상자나 통네는 게임 내의 다른 유닛에 부여된 능력 중 상당수가 부여되자 않을 것이다.

파괴 가능한 프랍에 너무 많은 메서드를 부여하는 인터페이스 한 개를 만드는 대신, 여러개의 작은 인터페이스로 분할하자
그러면 인터페이스를 구현하는 클래스에서 필요한 요소만 선택해 사용할 것이다.

 

 

 

 

public interface IMovable
{
    public float MoveSpeed { get; set; }
    public float Acceleration { get; set; }
    public void GoForward();
    public void Reverse();
    public void TurnLeft();
    public void TurnRight();
}
public interface IDamageable
{
    public float Health { get; set; }
    public int Defense { get; set; }
    public void Die();
    public void TakeDamage();
    public void RestoreHealth();
}
public interface IUnitStats
{
    public int Strength { get; set; }
    public int Dexterity { get; set; }
    public int Endurance { get; set; }
}

폭발하는 통에 IExplodable 인터페이스를 추가할 수도 있다.

 

 

 

public interface IExplodable
{
    public float Mass { get; set; }
    public float ExplosiveForce { get; set; }
    public float FuseDelay { get; set; }
    public void Explode();
}

클래스 하나가 둘 이상의 인터페이스를 구현할 수 있으므로 IDamageable, IMoveable,IUnitStats에서 적 유닛 코드를 작성할 수 있다.

폭발하는 통은 다른 인터페이스의 불필요한 오버헤드 없이 IDamageable과 IExplodable을 사용할 수 있다.

 

public class ExplodingBarrel : MonoBehaviour, IDamageable, IExplodable
{
... 
}
public class EnemyUnit : MonoBehaviour, IDamageable, IMovable,
IUnitStats
{
... 
}

리스코프 치환의 예시와 유사하게 여기에서도 상속보다 합성을 우선시한다.
인터페이스 분리 원칙은 시스템을 분리하고 간편하게 수정및 재배포하는 데 도움이 된다.

 

 

 

 

 

종속성 역전 원칙(DIP)

상위 수준의 모듈이 하위 수준의 모듈에서 어떤 것도 직접 가져오면 안된다고 명시한다.
양측 모두 추상화에 의존해야 한다.

다른 클래스와 관계가 있는 클래스는 종족 또는 결합 관계가 있다고 부른다.

소프트웨어 디자인에서 각 종속성은 약간의 위험성을 내포한다.

한 클래스가 다른 클래스의 작동 방식에 대해 너무 많이 아는 경우, 첫 번째 클래스를 수정하면 두 번째 클래스에 피해를 줄 수 있으며 그 반대의 경우도 마찬가지입니다. 결합도가 높으면 깔끔하지 않은 코드로 간주된다.

애플리케이션의 한 부분에서 오류가 발생하면 다른 부분으로 눈덩이처럼 확대될 수 있다.

클래스 간 종속성을 가능한 한 최소화하는 것이 이상적입니다. 또한 각 클래스의 내부 요소가 한결같이 작동해야 하며,

외부 연결에 의존하지 않아야 한다.

내부 로직이나 프라이빗 로직으로 작동하는 객체를 응집도가 높은 것으로 간주된다.

최고의 시나리오는 결합도는 낮추고 응집도는 높이는 것을 목표로 삼는다.

게임 애플리케이션을 수정하고 확장할 수 있어야 한다.

수정하기가 까다롭거나 쉽지 않다면 현재 어떻게 구조화되어 있는지 조사한다.

종속성 역전 원칙은 클래스 간의 결합도를 줄이는 데 도움이 될 수 있다.

애플리케이션에서 클래스와 시스템을 만들 때 자연스럽게 일부는 상위 수준이 되고 일부는 하위 수준이 된다.

상위 수준 클래스는 하위 수준 클래스에 의존해 작업을 수행하는데, SOLID 원칙에서는 이를 바꿔야 한다고 강조한다.

캐릭터가 레벨을 탐험하며 문을 트리거해 여는 게임을 개발한다고 가정하자.

Switch라는 클래스와 Door라는 클래스를 만들게 될 것이다.

상위 수준에서는 캐릭터가 특정 위치로 이동하고 특정 해동이 발생하기를 원하는데, Switch가 이러한 행동을 담당한다.

하위 수준에서는 다른 클래스인 Door가 있으며, 여기에는 문 지오메트리를 여는 방법의 실제 구현이 포함되어 있다.

간소화를 목적으로 Debug.Log 문이 추가되어 문을 열고 닫는 로직을 나타낸다.

 

 

 

public class Switch : MonoBehaviour
{
    public Door door;
    public bool isActivated;
    public void Toggle()
    {
        if (isActivated)
        {
            isActivated = false;
            door.Close();
        }
else {
            isActivated = true;
            door.Open();
        }
} }
public class Door : MonoBehaviour
{
    public void Open()
    {
        Debug.Log("The door is open.");
    }
    public void Close()
    {
        Debug.Log("The door is closed.");
    }
}

Switch는 Toggle 메서드를 호출해 문을 열고 닫을 수 있다.
작동하기는 하지만 이 경우 Door에서 직접 Switch로 연결되는 종속성이 발생한다는 문제가 있다.
Switch의 로직이 Door외의 다른 항목, 이를테면 조명을 켜거나 거대 로봇을 활성화하는 경우에도 사용되어야 한다면 어떨까?

Switch 클래스에 메서드를 추가할 수 있겠지만, 그러면 개방 폐쇄 원칙을 위반하게 되고,

기능을 확장하려 할 때마다 원본 코드를 수정해야 한다.

이번에도 추상화로 문제를 해결할 수 있다.

클래스 사이에 ISwitchable이라는 인터페이스를 삽입할 수 있다.

ISwitchable에 필요한 것은 액티브 상태인지 알기 위한 public 프로퍼티, 그리고 Activate 및 Deactiveate라는 메서드 2개뿐이다.

 

 

 

public interface ISwitchable
{
    public bool IsActive { get; }
    public void Activate();
    public void Deactivate();
}

그러면 Switch는 아래와 같이 구성되며, 문에 직접 의존하지 않고 ISwitchable 클라이언트에 의존한다.

 

 

 

public class Switch : MonoBehaviour
{
    public ISwitchable client;
    public void Toggle()
    {
        if (client.IsActive)
        {
            client.Deactivate();
        }
        else 
        {
            client.Activate();
           }
    } 
}

한편 ISwitchable을 구현하려면 Door를 재작업해야 한다.

 

 

public class Door : MonoBehaviour, ISwitchable
{
    private bool isActive;
    public bool IsActive => isActive;
    public void Activate()
    {
        isActive = true;
        Debug.Log("The door is open.");
    }
    public void Deactivate()
    {
        isActive = false;
        Debug.Log("The door is closed.");
    }
}

이제 종속성을 역전했다.
인터페이스가 스위치를 문에 배타적으로 고정하지 않고,
둘 사이에 추상화를 형성한다.
Switch는 더 이상 문의 특정한 메서드(Open 및 Close)에 직접 의존하지 않는다.
대신 ISwitchable의 Activete 및 Deactivate를 사용한다.

작지만 특별한 이 변화로 재사용성이 향상된다.
이전에는 Switch가 Door에만 작동했으나,

이제 ISwitchable을 구현하는 모든 요소에 작동한다.

따라서 Switch가 활성화할 수 있는 클래스를 더 많이 만들 수 있다.

함정 문이든 레이저 빔이든 높은 수준의 Switch가 작동한다.

ISwitchable을 구현하는 호환 가능한 클라이언트만 있으면 된다.

다른 SOLID 항목들처럼 종속성 역전 원칙에서도 일반적을 ㅗ클래스 간 관계를 설정하는 방식을 검토하도록 요구한다.

결합도를 낮춰 프로젝트를 간편하게 확장/축소하자

 

 

 

 

 

 

인터페이스와 추상 클래스 비교

추상 클래스

  • abstract 키워드를 사용해 기본 클래스를 정의할 수 있으므로, 상속을 통해 일반적인 기능(메서드,필드,상수 등)을 서브 클래스로 전달할 수 있다.
  • 추상 클래스는 직접 인스턴스화할 수 없으며, 대신 구상 클래스를 파생해야 한다.
  • 인터페이스를 사용하는 대신, 구상 클래스(예 : Light 또는 Door)를 Switchable이라는 이름의 추상 클래스로부터 파생시킨다.
  • 상속은 is a 관계를 정의한다. (모두 켜고 끌 수 있는 전환 가능한(switchable) 요소이다.
  • 추상 클래스에는 정적 멤법뿐 아니라 필드 및 상수도 가질 수 있다는 장점이 있다.
  • 또한 protected 및 private처럼 더 제한된 액세스 한정자도 적용할 수 있다.
  • 인터페이스와는 달리, 추상 클래스를 사용하면 구상 클래스 간의 핵심 기능을 공유하도록 지원하는 로직을 구현할 수 있다.
  • 서로 다른 기본 클래스 2개의 특징을 가지는 파생 클래스를 생성할 필요가 없다면 상속을 문제 없이 이용할 수 있다.
  • C#에서는 둘 이상의 기본 클래스로부터 상속할 수 없다.
  • 게임의 모든 로봇에 대해 또 다른 추상 클래스를 설정해 두었다면, 어떤 클래스에서 파생시킬지 결정하기가 더 어렵다. Robot 또는 Switchable, 두 기본 클래스중에서 무엇을 사용해야 할까?

인터페이스

  • 인터페이스 분리 원칙에서 보았듯이, 무언가 상속 패러다임에 제대로 부합하지 않을 때 인터페이스는 더 많은 유연성을 제공한다.
  • has a 관계를 이용해 더 쉽게 고르고 선택할 수 있다.
  • 하지만 인터페이스에는 멤버의 선언만 있다.
  • 특정 로직을 구체화하는 책임은 인터페이스를 실제로 구현하는 클래스에 있게 된다.
  • 따라서 항상 둘 중 하나를 선택하게 되는 않는다.
  • 추상 클래스를 사용해 코드를 공유하려는 기본 기능을 정의하고, 인터페이스를 사용해 유연성이 필요한 주변 기능을 정의하자.
추상 클래스 인터페이스
메서드 전체 또는 일부를 구현 메서드를 선언하지만 구현할 수는 없다.
변수와 필드를 선언/사용 메서드와 프로퍼티만 선언(필드는 제외)
정적 멤버 보유 정적 멤버 선언/사용 불가
생성자 사용 생성자 사용 불가
모든 액세스 한정자 사용(protected private 등) 액세스 한정자 사용 불가(모든 멤버는 암묵적으로 public)


중요 : 클래스는 최대 하나의 추상 클래스에서 상속될 수 있지만, 여러 인터페이스를 구현할 수 있다.

 

 

 

SOLID 원칙의 이해

SOLID 원칙은 일상적인 실무를 통해 알아가는 것이다.
코딩을 하며 항상 참고해야 할 다섯 가지 기본 규칙이라 생각하자.

  • 단일 책임 : 클래스는 단 하나의 작업만 수행하고 단 하나의 이유로만 변경할 수 있어야 한다.
  • 개방 폐쇄 : 기존 작동 방식을 변경하지 않고 클랫의 기능을 확장할 수 있어야 한다.
  • 리스코프 치환 : 서브 클래스는 기본 클래스로 대체될 수 있어야 한다.
  • 인터페이스 분리 : 인터페이스는 최소한의 메서드로 짧게 유지한다. 클라이언트는 필요한 항목만 구현한다.
  • 종속성 역전 : 추상화에 의존하자. 하나의 구상 클래스에서 다른 구상 클래스로 직접 의존하지 말아야 한다.

참고 문헌: Unity_Level up your code with Game Programming Pattern-3-ko_kr

728x90

'게임엔진 > Unity' 카테고리의 다른 글

Unity - OnMouseDown  (0) 2024.07.02
Unity - GetAxis  (0) 2024.07.02
Unity - 선형 보간  (0) 2024.07.02
Unity - Coroutine  (0) 2024.07.01
유니티 - InspectorAttribute  (0) 2024.05.03

+ Recent posts