728x90

Invoke

Invoke 함수를 사용하면 메서드가 나중에 호출되도록 예약할 수 있습니다. 이 영상에서는 Unity 스크립트에서 Invoke, InvokeRepeating, CancelInvoke 등의 함수를 사용하는 방법을 알아봅니다.

InvokeScript

using UnityEngine;
using System.Collections;

public class InvokeScript : MonoBehaviour
{
    public GamaObject target;

    private void State()
    {
        Inboke(nameof(SpawnObject),2);
    }

    private SpawnObject()
    {
        Instantiate(target, new Vector3(0,2,0),Quaternion.identity);
    }
}

InvokeRepeating

using UnityEngine;
using System.Collections;

public class InvokeRepeating : MonoBehaviour 
{
    public GameObject target;


    void Start()
    {
        InvokeRepeating("SpawnObject", 2, 1);
    }

    void SpawnObject()
    {
        float x = Random.Range(-2.0f, 2.0f);
        float z = Random.Range(-2.0f, 2.0f);
        Instantiate(target, new Vector3(x, 2, z), Quaternion.identity);
    }
}

참고 : 유니티 튜토리얼

728x90

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

Unity - 인스턴스화  (0) 2024.07.02
Unity - Class  (0) 2024.07.02
Unity - 데이터 유형  (0) 2024.07.02
Unity - GetComponent  (0) 2024.07.02
Unity - OnMouseDown  (0) 2024.07.02
728x90

인스턴스화

Instantiate 함수를 사용하여 런타임 시 프리팹의 클론을 생성하는 방법을 알아봅니다.

UsingInstantiate

using UnityEngine;
using System.Collections;

public class UsingInstantiate : MonoBehaviour
{
    public Rigidbody rocketPrefab;
    public Transform barrelEnd;


    void Update ()
    {
        if(Input.GetButtonDown("Fire1"))
        {
            Rigidbody rocketInstance;
            rocketInstance = Instantiate(rocketPrefab, barrelEnd.position, barrelEnd.rotation) as Rigidbody;
            rocketInstance.AddForce(barrelEnd.forward * 5000);
        }
    }
}

&nbsp

RocketDestruction

```
using UnityEngine;
using System.Collections;

public class RocketDestruction : MonoBehaviour
{
void Start()
{
Destroy (gameObject, 1.5f);
}
}
``

728x90

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

Unity - Invoke  (0) 2024.07.02
Unity - Class  (0) 2024.07.02
Unity - 데이터 유형  (0) 2024.07.02
Unity - GetComponent  (0) 2024.07.02
Unity - OnMouseDown  (0) 2024.07.02
728x90

Class

클래스를 사용하여 정보를 저장하고 구성하는 방법과, 클래스의 일부와 함께 작동하는 생성자를 생성하는 방법을 알아봅니다.

SingleCharacterScript

using UnityEngine;
using System.Collections;

public class SingleCharacterScript : MonoBehaviour
{
    public class Stuff
    {
        public int bullets;
        public int grenades;
        public int rockets;

        public Stuff(int bul, int gre, int roc)
        {
            bullets = bul;
            grenades = gre;
            rockets = roc;
        }
    }


    public Stuff myStuff = new Stuff(10, 7, 25);
    public float speed;
    public float turnSpeed;
    public Rigidbody bulletPrefab;
    public Transform firePosition;
    public float bulletSpeed;


    void Update ()
    {
        Movement();
        Shoot();
    }


    void Movement ()
    {
        float forwardMovement = Input.GetAxis("Vertical") * speed * Time.deltaTime;
        float turnMovement = Input.GetAxis("Horizontal") * turnSpeed * Time.deltaTime;

        transform.Translate(Vector3.forward * forwardMovement);
        transform.Rotate(Vector3.up * turnMovement);
    }


    void Shoot ()
    {
        if(Input.GetButtonDown("Fire1") && myStuff.bullets > 0)
        {
            Rigidbody bulletInstance = Instantiate(bulletPrefab, firePosition.position, firePosition.rotation) as Rigidbody;
            bulletInstance.AddForce(firePosition.forward * bulletSpeed);
            myStuff.bullets--;
        }
    }
}

Inventory

sing UnityEngine;
using System.Collections;

public class Inventory : MonoBehaviour
{
    public class Stuff
    {
        public int bullets;
        public int grenades;
        public int rockets;
        public float fuel;

        public Stuff(int bul, int gre, int roc)
        {
            bullets = bul;
            grenades = gre;
            rockets = roc;
        }

        public Stuff(int bul, float fu)
        {
            bullets = bul;
            fuel = fu;
        }

        // 생성자
        public Stuff ()
        {
            bullets = 1;
            grenades = 1;
            rockets = 1;
        }
    }


    // Stuff 클래스의 인스턴스(오브젝트) 생성
    public Stuff myStuff = new Stuff(50, 5, 5);

    public Stuff myOtherStuff = new Stuff(50, 1.5f);

    void Start()
    {
        Debug.Log(myStuff.bullets); 
    }
}

MovementControls

using UnityEngine;
using System.Collections;

public class MovementControls : MonoBehaviour
{
    public float speed;
    public float turnSpeed;


    void Update ()
    {
        Movement();
    }


    void Movement ()
    {
        float forwardMovement = Input.GetAxis("Vertical") * speed * Time.deltaTime;
        float turnMovement = Input.GetAxis("Horizontal") * turnSpeed * Time.deltaTime;

        transform.Translate(Vector3.forward * forwardMovement);
        transform.Rotate(Vector3.up * turnMovement);
    }
}

Shooting

using UnityEngine;
using System.Collections;

public class Shooting : MonoBehaviour
{
    public Rigidbody bulletPrefab;
    public Transform firePosition;
    public float bulletSpeed;


    private Inventory inventory;


    void Awake ()
    {
        inventory = GetComponent<Inventory>();
    }


    void Update ()
    {
        Shoot();
    }


    void Shoot ()
    {
        if(Input.GetButtonDown("Fire1") && inventory.myStuff.bullets > 0)
        {
            Rigidbody bulletInstance = Instantiate(bulletPrefab, firePosition.position, firePosition.rotation) as Rigidbody;
            bulletInstance.AddForce(firePosition.forward * bulletSpeed);
            inventory.myStuff.bullets--;
        }
    }
}

참고 : 유니티 튜토리얼

728x90

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

Unity - Invoke  (0) 2024.07.02
Unity - 인스턴스화  (0) 2024.07.02
Unity - 데이터 유형  (0) 2024.07.02
Unity - GetComponent  (0) 2024.07.02
Unity - OnMouseDown  (0) 2024.07.02
728x90

데이터 유형

변수의 작동 방식을 정확히 이해할 수 있도록 값과 레퍼런스 데이터 유형의 주요 차이점에 대해 알아볼까요?

Value

  • int
  • float
  • double
  • bool
  • char
  • Structs
    • Vector3
    • Quaternion

 

 

Reference

  • Classes
    • Transform
    • GameObject

 

 

DatatypeScript

using UnityEngine;
using System.Collections;

public class DatatypeScript : MonoBehaviour 
{
    void Start () 
    {
        //값 유형 변수
        Vector3 pos = transform.position;
        pos = new Vector3(0, 2, 0);

        //레퍼런스 유형 변수
        Transform tran = transform;
        tran.position = new Vector3(0, 2, 0);
    }
}

참고 : 유니티 튜토리얼

728x90

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

Unity - 인스턴스화  (0) 2024.07.02
Unity - Class  (0) 2024.07.02
Unity - GetComponent  (0) 2024.07.02
Unity - OnMouseDown  (0) 2024.07.02
Unity - GetAxis  (0) 2024.07.02
728x90

GetComponent

GetComponent 함수를 사용하여 다른 스크립트 또는 컴포넌트의 프로퍼티를 지정하는 방법을 알아봅니다.

가급적이면 Awake(), Start()에 한번만 호출하는 것이 좋다.

UsingOtherComponents

using UnityEngine;
using System.Collections;

public class UsingOtherComponents : MonoBehaviour
{
    public GameObject otherGameObject;


    private AnotherScript anotherScript;
    private YetAnotherScript yetAnotherScript;
    private BoxCollider boxCol;


    void Awake ()
    {
        anotherScript = GetComponent<AnotherScript>();
        yetAnotherScript = otherGameObject.GetComponent<YetAnotherScript>();
        boxCol = otherGameObject.GetComponent<BoxCollider>();
    }


    void Start ()
    {
        boxCol.size = new Vector3(3,3,3);
        Debug.Log("The player's score is " + anotherScript.playerScore);
        Debug.Log("The player has died " + yetAnotherScript.numberOfPlayerDeaths + " times");
    }
}

AnotherScript

using UnityEngine;
using System.Collections;

public class AnotherScript : MonoBehaviour
{
    public int playerScore = 9001;
}

YetAnotherScript

using UnityEngine;
using System.Collections;

public class YetAnotherScript : MonoBehaviour
{
    public int numberOfPlayerDeaths = 3;
}

참고 : 유니티 튜토리얼

728x90

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

Unity - Class  (0) 2024.07.02
Unity - 데이터 유형  (0) 2024.07.02
Unity - OnMouseDown  (0) 2024.07.02
Unity - GetAxis  (0) 2024.07.02
Unity - 선형 보간  (0) 2024.07.02
728x90

OnMouseDown

콜라이더 또는 GUI 요소에 대한 마우스 클릭을 감지하는 방법을 알아봅니다.

MouseClick

using UnityEngine;
using System.Collections;

public class MouseClick : MonoBehaviour
{

    private Rigidbody rb;

    private void Awake()
    {
        rb = GetComponent<Rigidbody>();
    }

    void OnMouseDown ()
    {
        rb.AddForce(-transform.forward * 500f);
        rb.useGravity = true;
    }
}

출처 : 유니티 튜토리얼

728x90

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

Unity - 데이터 유형  (0) 2024.07.02
Unity - GetComponent  (0) 2024.07.02
Unity - GetAxis  (0) 2024.07.02
Unity - 선형 보간  (0) 2024.07.02
Unity - Coroutine  (0) 2024.07.01
728x90

GetAxis

Unity에서 게임 입력에 기반하여 축을 가져오는 방법을 살펴보고, 이러한 축을 Unity 입력 관리자를 통해 어떻게 수정할 수 있는지 알아보겠다.

AxisExample

using UnityEngine;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;

public class AxisExample : MonoBehaviour
{
    public float range;
    public Text textOutput;


    void Update () 
    {
        float h = Input.GetAxis("Horizontal");
        float xPos = h * range;

        transform.position = new Vector3(xPos, 2f, 0);
        textOutput.text = "Value Returned: " + h.ToString("F2");  
    }
}

AxisRawExample

using UnityEngine;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;

public class AxisRawExample : MonoBehaviour
{
    public float range;
    public Text textOutput;


    void Update () 
    {
        float h = Input.GetAxisRaw("Horizontal");
        float xPos = h * range;

        transform.position = new Vector3(xPos, 2f, 0);
        textOutput.text = "Value Returned: " + h.ToString("F2");  
    }
}

DualAxisExample

using UnityEngine;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;

public class DualAxisExample : MonoBehaviour 
{
    public Text horizontalValueDisplayText;
    public Text verticalValueDisplayText;
    public float hRange;
    public float vRange;

    void Update ()
    {
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");
        float xPos = h * hRange;
        float yPos = v * vRange;

        transform.position = new Vector3(xPos, 0, vPos);
        horizontalValueDisplayText.text = h.ToString("F2");
        verticalValueDisplayText.text = v.ToString("F2");
    }
}

 

 

참고 : 유니티 튜토리얼

728x90

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

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

게임 제작 시 두 값을 선형적으로 보간하면 유용한 경우가 있다.
선형 보간은 Lerp라는 함수를 통해 이루어진다.

선형 보간은 주어진 두 값 사이의 일정 비율에 해당하는 값을 찾는 것
예를 들어 숫자 3과 5를 50%로 선형적으로 보간한 결과는 숫자 4이다.
4는 3과 5 사이의 50% 지점에 있기 때문

Unity에는 다양한 유형에 사용 가능한 여러 Lerp 함수가 있다.

// 이경우, 결과 = 4
float result = Mathf.Lerf(3f,5f,0.5f);

 

Mathf.Lerp 함수는 3개의 float 파라미터를 사용

  • 하나는 보간 시작값
  • 다른 하나는 보간 끝 값
  • 마지막 float는 보간 거리를 나타낸다.
    이 경우 보간 값은 0.5로 50%를 의미
    값이 0이면 이 함수는 '시작'값을 반환하고, 값이 1이면 '끝' 값을 반환

 

Lerp함수의 다른 예로는 Color.Lerp와 Vector3.Lerp가 있다.
이 함수는 Mathf.Lerp와 완전히 동일하게 작동하지만 시작 및 끝 값이 Color 유형과 Vector3 유형이다.
각각의 경우에 세 번째 파라미터는 그대로 보간되는 양을 나타내는 float이다.
이 두 함수의 결과는 각각 특정 색상 2개를 블렌딩한 색상, 특정 벡터 2개 사이의 일정 비율에 해당하는 벡터가 되겠다.

Vector3 from = new Vector3 (1f,2f,3f);
Vector3 to = new Vector3 (5f,6f,7f);

// 이 경우 결과 = (4,5,6)
Vector3 result = Vector3.Lerp(from,to,0.75f);

이 경우 결과는 (4,5,6)이다.
4는 1과 5사이의 75% 지점에 있고, 5는 2와 6사이의 75% 지점에 있으며, 6은 3과 7의 75% 지점에 있기 때문

Color.Lerp를 사용하는 경우에도 동일한 원이 적용된다.
Color 구성에서 색상은 빨간색, 파란색, 초록색, 알파를 나타내는 4개의 float로 표현
Lerp 사용 시 이러한 float는 Mathf.Lerp 및 Vector3.Lerp와 마찬가지로 보간

상황에 따라 시간이 지나며 값을 매끄럽게 전환하는 데 Lerp 함수를 사용할 수 있음.

 

 

void Update()
{
    light.intensity = Mathf.Lerp(light.intensity,8f,0.5f);
}

0에서 시작하는 광원 강도는 첫 Update 호출 후 4로 설정되며 그 다음 프레임은 6,7,7.5 등으로 설정
따라서 여러 프레임을 거치면서 광원 강도는 8로 향하게 되지만 변경 속도는 목표에 다가갈수록 느려진다.
강도는 여러 프레임을 거치면서 변한다.
프레임 속도에 구애받지 않으려면 다음 코드를 사용하면 된다.

 

 

void Update()
{
     light.intensity = Mathf.Lerp(light.intensity, 8f, 0.5f * Time.deltaTime);
}

 

그러면 강도는 프레임이 아닌 초를 기준으로 변경된다.

값을 매끄럽게 전화하려는 경우 SmoothDamp 함수를 사용하는 것이 가장 좋을 수 있음.
매끄러운 전환 효과를 원하는 대로 구현할 수 있는 경우에만 Lerp를 사용하자.



참고 문헌 : 유니티 튜토리얼

728x90

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

Unity - OnMouseDown  (0) 2024.07.02
Unity - GetAxis  (0) 2024.07.02
Unity - Coroutine  (0) 2024.07.01
유니티 게임 프로그래밍 패턴 - SOLID 원칙  (0) 2024.06.19
유니티 - InspectorAttribute  (0) 2024.05.03
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

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