TIL - 10

Chu_uhC·2023년 10월 9일
0

TIL

목록 보기
10/16
post-thumbnail

📄 23.10.9 🎈 한글날



📄 23.10.10 ✍ FSM - 1

✨유한 상태 기계(Finite-State Machine) : 유한한 상태의 기계 즉, 설계자가 상태의 개수를 지정하고 기계는 현재 상태 하나만을 가지고 있다. 그리고 임의의 사건을 통하여 상태들끼리 서로 전이(Transition)된다.

🔨이번에 만들어 볼 두 가지의 FSM

먼저 플레이어의 경우 강의의 복습, NPC는 직접 만들어낸 역사적인 첫 FSM이 되겠다.

플레이어 FSM는 Ground에 속하는 두 가지의 상태와 추락 상태 총 3개가 있으며
NPC의 경우는 대기, 순찰, 주시 상태 총 3개가 있다.

🔨 플레이어 상태에 대한 설명

 상태        행동                   전이 조건           
Idle가만히 있는다오브젝트의 움직임을 감지하면 Move, Fall 둘 중 하나
Move입력한 키값에 맞게 힘을 더해준다중력을 감지하면 Fall, 움직임이 멈추면 Idle
Fall방향키 제거, 바닥 충돌하면 시간 체크바닥에서 0.5초가 지나면 Idle

💭 먼저 플레이어 FSM부터 설명하자면...
     그라운드에 속한 상태들 Idle, Move이어야지 Fall로 전이될 수 있다.
     당장에는 없어도 되는 부분이지만 염두를 해두고 만들었다는게 중요!

💭 중력을 Velocity로 감지하는데 버그 투성이이다.
     Ray를 쏘기에는 너무 비용이 비쌀 것 같고 반드시 새로운 방법을 찾아 낼 것

📌 Ground라는 상태는 정확히는 없다.
     1. 상태의 분류 2. 상속을 통한 코드 재사용을 위해 만들어졌다.

🔨 NPC 상태에 대한 설명

 상태        행동                   전이 조건           
Idle시간을 재면서 가만히 있는다2초가 경과하면 Patrol
Patrol목적지로 몸을 돌리고 나서 출발한다목적지 도착 직후, Idle
Gaze목표를 계속 쳐다본다외부에서 호출하면 변경, 목표가 범위를 벗어나면 Idle

💭 Gaze는 외부에서의 개입 없이는 전이될 수 없는데 이게 과연 옳은 방법인가?
     내부에서도 할 수 있지만 성능적인 측면에서 OnTrigerEnter로 전이되고 전이되는데
     객체지향에서 너무 벗어나는 것이 아닐까 걱정이다.
     아니면 오히려 FSM은 어떤 객체에 종속되어있기 때문에 자유롭게 사용해도 될까?

💭 상태들은 기본적으로 MonoBehaviour를 상속받지 못한다.
     즉, Coroutine, Invoke같은 시간과 관련된 함수의 실행이 불가능하거나 어지럽다.
     그렇다고 Update를 사용하기에는 너무 비효율적이다.

😋 아무튼 그래서!

첫 캠프 상담할 때 만들고 싶은 기능에 "오브젝트들의 AI"라고 했는데 이렇게 갑자기
하게 될 줄은 상상도 못했다.
남은 부분들은 바로 다음에 정리하도록 하고 누가 "너의 핵심 코딩 기술은 뭐니"라고 하면
"F.S.M"라고 할 수 있을 정도로 개념을 익혀보자.

🎉 한 장 요약



📄 23.10.11 ✍ FSM - 2

오늘은 전체적인 구조와 어떤 식으로 변경이 되는지 코드를 통하여 알아봅시다.

🔨 뚝딱뚝딱 클래스 다이어그램~

💭 Npc(Unit)을 제외하고는 MonoBehaviour를 상속을 받지 않는다.
     즉, Update를 처리하기위해서는 태초의 스크립트 Npc의 힘을 빌려야한다.
     그러면 오버라이드도 하고 base도 호출하다보면 오버헤드는 반드시 발생하게되지만
     복잡한 FSM이 아닌 이상 이 이슈는 신경쓰지 않아도 된다.

💭 StateMachine에서 처음 상태를 입력해주면 그 이후로는 조건에 맞게 스스로 전이되고
     그렇기에 새로운 상태를 조건만 추가해서 연결해준다면 상태에 맞는 함수들이 실행된다.
     이런 점들은 유연성과 직관적인 코드라는 정점들을 확인해볼 수 있다.

🔨 FSM 코드

public interface IState // 상태들이 사용할 인터페이스
{
    public void Enter(); // 상태 변경시 실행
    public void Exit(); // 종료될 때 실행
    public void Update(); 
    public void PhysicsUpdate();
}

// FSM의 본체
public class NpcStateMachine
{
    protected IState currentState; 
	// 현재 상태, 인터페이스를 활용한 전략 패턴이 사용되었다.
    
    public Npc npc;
    public Transform transform;
    // 접근의 효율을 위한 캐싱

    public IState IdleState { get; private set; }
    public IState PatrolState { get; private set; }
    public IState GazeState { get; private set; }
    // 보유하고있는 상태들
	
    // 초기화
    public NpcStateMachine(Npc npc)
    {
        this.npc = npc;
        transform = npc.transform;
        startPos = npc.transform.position;
        targetPos = npc.gazeTarget;

        IdleState = new NpcIdleState(this);
        PatrolState = new NpcPatrolState(this);
        GazeState = new NpcGazeState(this);
        
        ChangeState(IdleState);
        // 현재 상태가 null이기때문에 실행해주어야한다.
    }
	
    // 상태 변경 함수
    public void ChangeState(IState state)
    {
        currentState.Exit();
        currentState = state;
        state.Enter();
        // 현재 상태 종료 -> 상태 변경 -> 새로운 상태 초기화
    }
	
    // 이 함수가 `MonoBehaviour`가 있는 update에서 호출되어 사용된다.
    public void Update()
    {
        currentState?.Update();
    }
}

💭 구조는 생각보다 간단하고 복잡한 경우에는 시각화를 통해 효과적으로 이해를 도울 수있다.
     내외부에서 ChangeState()를 호출하면 상태가 바뀐다가 알파이자 오메가이다.
     그 의외에는 핵심적인 부분은 없는 것 같다.

💭 위에 말한 오버헤드를 신경쓰기 위해선 각 상태마다 매 프레임 호출되는 함수들을
     효율적으로 사용하기위해서는 캐싱이 필수적이다.

😋 아무튼 그래서!

FSM는 하는 기능들에 비해서 개념이 상당히 쉬운 느낌이다.
단 하나의 상태만 초기화, 업데이트, 종료가 반복된다. 이것만 기억하면 될 거같다.

🎉 한 장 요약



📄 23.10.12 ✍ 빌더 패턴

✨빌더 패턴(Builder Pattern) : 객체의 생성과 조립을 담당하는 클래스를 따로 만들어 사용하는 방식

생성과 관련된 만큼 생성자의 단점을 보완하기 위한 패턴이며 생성자를 조금만 복잡하게
사용해 본 적이 있다면 문제점들을 쉽게 이해할 수 있을 것이다.
이 문제점부터 한 단계씩 올라가 보며 빌더 패턴이 왜 만들어졌는지 이해해 보자.

🔨 생성자만을 사용하여 만들면 생기는 문제점

var smallGame = new SmallGame(player, monster)
// 생성 과정이 단순한 경우 굳이 빌더 패턴은 필요하지 않다.

var bigGame = new BigGame(player, monster, npc, item, null, dungeon, town ...)
// 생성자가 많아지면 가독성, 유지보수의 문제와 인자의 순서를 알아야한다는 불편함이 발생한다.
// 또한 필요없는 부분까지 작성해줘야하는 불필요함이 발생한다.

이런 문제점을 해결하기 위해서 점층적 생성자 패턴을 사용할 수 있다.
setter라는 값의 초기화를 담당하는 함수를 만들어 이용하는 방식이다.

🔨 점층적 생성자 패턴로 문제 해결해보기

class BigGame
{
	Player player;
    Monster monster;
    ...
    
    // setter 역할
    public void SetPlayer(Player player)
    {
    	this.player = player;
    }
    public void SetMonster(Monster monster)
    {
    	this.monster = monster;
    }
}

// 함수의 생성과 초기화
var bigGame = new BigGame();
bigGame.SetPlayer(player);
bigGame.SetMonster(monster);

// 생성자만 사용했을 때와는 달리 가독성 있게 무엇이 초기화되었는지 알 수 있지만
// 많아질수록 클래스의 코드들이 길어지고 지저분 해진다.
// 하지만 public이 사용을 되었기에 당연히 캡슐화는 위배되고 가장 중요한 일관성 문제가 발생한다.
// 그 이유는, 개체가 변수에 할당된 이후에 setter들이 값을 지정해 주는데 모든 setter 함수들이
// 실행되기 전에 다른 쓰레드에서 접근을 해버리면 오류가 발생할 수도 있다.
// 

그러면 여기서 가장 큰 문제인 생성이 된 이후 값의 초기화가 이루어지는 문제점을
생성과 초기화를 담당하는 빌더 클래스를 만들어 해결해보자.

🔨 빌더 클래스

class GameBuilder
{
	BigGame bigGame;
    
    // new에 해당하는 부분
    public BigGame Build()
    {
    	return bigGame;
    }
    
    // 생성자 역할을 할 함수들
    public GameBuilder Player(Player player)
    {
    	bigGame.Player = player;
    	return this;
    }
    public GameBuilder Monster(Monster monster)
    {
    	bigGame.Monster = monster;
    	return this;
    }
}

// 리스트(변수)에 담아두는 이유는 객체의 생성을 원하는 순간에 실행할 수 있다. 
List<GameBuilder> gameBuilder = List<GameBuilder>();
// 초기화 부분
gameBuilder.Add(
	new GameBuilder().Player(player)
	.Monster(monster)
);
// 생성 부분
var bigGame = gameBuilder[0].Build();


// BigGame 객체는 사실 초기화 부분에서 이루어진다 하지만 Build 함수를 호출하여 반환받지않으면
// 은닉화 되어있어 접근 할 수 없다.

📌 빌더 패턴이 만들어진 시기가 오래된 만큼 현재에는 IDE의 발전으로 잘 사용하지 않는다.
     하지만 필수 변수와 선택 변수를 선택할 수 있다는 것은 아직까지도 유효한 장점이다.

이렇게 초기화, 생성 부분이 빌더 클래스를 통하여 분리 되어 초기화가 끝나고
생성할 수 있으므로 일관성 부분은 해결되었다.

하지만 이런 빌더 패턴에도 단점이 있는데

📌 객체 1개당 빌더 1개이니 코드의 복잡도가 상승한다.

그래서 요약해보면 생성자로 인해 불편을 겪을 때 사용해볼만한 패턴이 아닐까 생각한다.

🎉 한 장 요약



📄 23.10.13 ✍ C# - 메모리 복습

✨메모리(Memory) : 우리가 프로그램을 시작하면 메모리에 그 정보들이 올라간다.
지금 구동하고있는 OS, 브라우저 같은 것들도 메모리에 할당되어 있다.
그리고 이제부터 실행할 우리 프로그램도 메모리에 할당 될 것이다.

CODEDATAHEAPSTACK
프로그램 코드정적 변수사용자의 동적 할당지역 변수와 매개변수

📌 CODE : 우리가 실행할 프로그램의 코드의 영역, CPU가 여기서 코드를 들고간다!

📌 DATA : static으로 선언된 변수들이 들어가기 때문에 new없이도 접근 가능!

📌 HEAP : 참조형의 데이터

📌 STACK : 값형의 데이터, 이름 그대로 stack의 형식으로 데이터가 저장되고 사라진다.

class CS // CODE
{
    public static int num; // num : DATA
	private int num2; // num2 : HEAP

    int Plus(int input) // Plus : CODE
    {
        int sum = num + input; // sum, input : STACK
        // num은 DATA 영역을 참조한다.
        
        return sum;
    }
}

실행 부분
CS cs = new cs(); // cs : HEAP
Console.Write(cs.Plus(10)); // Plus, 10(Literal) : CODE

🔨 실행 부분을 좀 더 나누어서 확인해보자

CS cs = new cs();
Console.Write(cs.Plus(10));

1. CS cs = new cs() // HEAP에 cs를 추가합니다
2. cs.Plus(10) // CODE에 접근하여 'Plus'를 가져온다
3. int sum = num + input // sum을 'STACK', num을 'DATA'불러오고, input을 'STACK'에 저장한다
4. Console.Write() // CODE에서 함수를 불러온다.

📌 언어마다 메모리의 원리는 똑같지만 조금씩 차이는 있을 수 있다.

📌 변수 = 참조값 일 경우, 32bit = 4byte 64bit = 8byte의 메모리를 할당받는다.
     그 이유는 2진수로 저장이 되는데 주소가 사용 환경에 맞춰 생성된다.

📌 100개의 cs가 생성되었을 때 plus의 함수도 100개가 저장이 될까?
     함수도 결국 메모리에 주소 값에 저장되어 있으며 CODE 영역에서 참조만 하면된다.

🎉 한 장 요약



📔 주간 결산

1. UI 과제에 대한 피드백
리소스를 관리할 때는 배열보다는 명시적으로 변수마다 할당을 해주는 것이 좋다.

Sprite[] uiImage (X)
Sprite hpImg; Sprite mpImg; (O) 

UI 작동 방식을 좀 더 확장성있게 사용하는 것이 좋다.

public void UI_1_Open(){} (X)
public void UI_2_Open(){} (X)
--------------------------------
public void UIOpen(UI ui){} (O)

MVC는 Model과 View는 의존성 없이 고유하게 짜고 Controller가 조종하는 형태
Cocoa MVC를 잘 사용하였음

버튼의 경우 클래스를 따로 만드는 방식보다 UI에 포함시키는 것이 좋다.

class button (X)
{
	public OpenUI(){};
}
-------------------------
class UI (O)
{
	public OpenUI(){};
}

2. 공중이 아닌지 체크하는 방법들
FSM을 만들 때 이용한 Y값의 변화를 이용한 방식은 너무나도 잘못된 접근 방식이다.
경사로를 내려가는 상태가 되면 공중인 상태가 된다.

해결 방법이 현재로써는 2가지가 있는데..
1. Ray를 활용하여 캐릭터의 길이보다 Ray의 길이가 긴지 체크하는 방법
2. 발 밑에 상태를 체크할 Collider를 만든 다음 Trigger에 따라 변경하는 방법

현재까지는 2번 방법이 비용도 적고 쉽게 구현가능 한 방법인 것 같다.

profile
ChuNyan

0개의 댓글