유니티와 디자인 패턴 - 下

이준호·2024년 1월 14일
0

📌 오브젝트 풀 패턴으로 최적화 하기

  • 프레임 속도를 유지하려면, 자주 생성되는 요소를 일부 메모리에 예약하는게 좋다.
  • 최근 죽인 적을 메모리에서 업애는 대신 다시 사용할 수 있도록 오브젝트 풀에 추가.
  • 엔티티의 새로운 인스턴스를 로드하는 초기의 촉기화 비용이 들지 않는다.
  • Unity에는 오브젝트 풀링이 API에 구현되어 있다.

➔ 오브젝트 풀 패턴

  • ObjectPool : 객체의 풀을 관리한다. 즉, 객체를 생성하고, 관리하고, 파기하는 책임

  • ReusablePool : 실제로 재사용될 객체이다. ObjectPool이 여기 객체들을 관리한다.

  • Client : 클라이언트는 필요할 때 ObjectPool에서 ReusablePool 객체를 요청하고, 사용 후에는 다시 풀에 반환한다.

장점

  • 예측할 수 있는 메모리 사용

  • 성능의 향상

단점

  • 이미 C#이 메모리 최적화가 뛰어나서 굳이 필요없다는 이야기가 있다.

  • 객체가 예측이 불가능하게 된다. (예측 불가능한 객체 상태)




➔ Client.cs

public class ClientObjectPool : MonoBehaviour
{
    private DroneObjectPool _pool;
    
    void Start()
    {
        _pool = gameObject.AddComponent<DroneObjectPool>();
    }

    void OnGUI()
    {
        if (GUILayout.Button("Spawn Drones"))
            _pool.Spawn();
    }
}



➔ Drone.cs

public class Drone : MonoBehaviour 
{
    public IObjectPool<Drone> Pool { get; set; }

    public float _currentHealth;

    [SerializeField] 
    private float maxHealth = 100.0f;
    [SerializeField] 
    private float timeToSelfDestruct = 3.0f;

    void Start() 
    {
        _currentHealth = maxHealth;
    }
    
    void OnEnable() 
    {
        AttackPlayer();
        StartCoroutine(SelfDestruct());
    }

    private void OnDisable() 
    {
        ResetDrone();
    }

    IEnumerator SelfDestruct() {
        yield return new WaitForSeconds(timeToSelfDestruct);
        TakeDamage(maxHealth);
    }
    
    private void ReturnToPool() {
        Pool.Release(this);
    }
    
    private void ResetDrone() {
        _currentHealth = maxHealth;
    }

    public void AttackPlayer() {
        Debug.Log("Attack player!");
    }

    public void TakeDamage(float amount) {
        _currentHealth -= amount;
        
        if (_currentHealth <= 0.0f)
            ReturnToPool();
    }
}



➔ ObjectPool.cs

public class DroneObjectPool : MonoBehaviour
{
    public int maxPoolSize = 10;
    public int stackDefaultCapacity = 10;

    // 필요할 떄 오브젝트 풀을 생성함
    public IObjectPool<Drone> Pool 
    {
        get 
        {
            if (_pool == null)
                _pool = 
                    new ObjectPool<Drone>(
                        CreatedPooledItem, 
                        OnTakeFromPool, 
                        OnReturnedToPool, 
                        OnDestroyPoolObject, 
                        true, 
                        stackDefaultCapacity,
                        maxPoolSize);
            return _pool;
        }
    }

    private IObjectPool<Drone> _pool;

    private Drone CreatedPooledItem() 
    {
        var go = 
            GameObject.CreatePrimitive(PrimitiveType.Cube);
        
        Drone drone = go.AddComponent<Drone>();
        
        go.name = "Drone";
        drone.Pool = Pool;
        
        return drone;
    }

    // 드론이 풀로 돌아올 때 호출됨
    private void OnReturnedToPool(Drone drone) 
    {
        drone.gameObject.SetActive(false);
    }

    // 드론이 풀에서 꺼내질 때 호출됨
    private void OnTakeFromPool(Drone drone) 
    {
        drone.gameObject.SetActive(true);
    }

    // 드론 오브젝트를 파괴할 때
    private void OnDestroyPoolObject(Drone drone) 
    {
        Destroy(drone.gameObject);
    }

    // 무작위 수의 드론을 생성하고 위치를 설정할 때
    public void Spawn() 
    {
        var amount = Random.Range(1, 10);
        
        for (int i = 0; i < amount; ++i) {
            var drone = Pool.Get();
            
            drone.transform.position = 
                Random.insideUnitSphere * 10;
        }
    }
}











📌 전략 패턴으로 드론 구현하기

  • 드론의 다양한 동작을 구현하는 상황
  • 런타임에 특정 동작을 객체에 바로 할당할 수 있다.

➔ 전략 패턴 개요

  • Context : 자신의 작업을 수행하는데 필요한 전략을 선택하는 클래스

  • Strategy : 전략 인터페이스를 구현한 클래스들로, 특정 행동을 제공한다.

  • Client : 클라이언트에서 Context 클래스를 생성

장점

  • 캡슐화가 잘될 수 있다.

  • 런타임에 객체가 사용하는 알고리즘을 교환할 수 있다.

단점

  • 전략 패턴과 상태 패턴이 혼동될 수 있다. 구조가 유사하지만 의도가 매우 다르다.
    - 전략 패턴 : 같은 문제를 해결하는 여러 알고리즘이 있을 때, 이들 중 하나를 런타임에 선택해야할 때. (ex : 상황에 따라 적합한 정렬 알고리즘 고르기), 즉, 알고리즘의 선택에 중점
    • 상태 패턴 : 객체가 여러 상태를 가지고 있고, 상태에 따라 행동이 달라져야할 때. 즉, 상태에 따른 행동 변경



➔ Drone.cs

public class Drone : MonoBehaviour {
    public void ApplyStrategy(IBehaviour strategy) {
        strategy.Maneuver(this);
    }
}



➔ Stategy.cs

public class ClientStrategy : MonoBehaviour {
    
    private GameObject _drone;
    private List<IBehaviour> 
        _components = new List<IBehaviour>();
    
    private void SpawnDrone() {
        _drone = 
            GameObject.CreatePrimitive(PrimitiveType.Cube);
        
        _drone.AddComponent<Drone>();
        
        _drone.transform.position = 
            Random.insideUnitSphere * 10;
        
        ApplyRandomStrategies();
    }

    private void ApplyRandomStrategies() {
        _components.Add(
            _drone.AddComponent<Weaving>());
        _components.Add(
            _drone.AddComponent<Bopping>());
        _components.Add(
            _drone.AddComponent<Fallback>());
        
        int index = Random.Range(0, _components.Count);
        
        _drone.GetComponent<Drone>().
            ApplyStrategy(_components[index]);
    }
    
    void OnGUI() {
        if (GUILayout.Button("Spawn Drone")) {
            SpawnDrone();
        }
    }
}











📌 커맨드 패턴으로 리플레이 시스템 구현하기

  • 커맨드 패턴은 게임 내에서 발생하는 모든 행동 (이동, 점프)을 명령으로 캡슐화를 할 수 있다. 그리고 이 명령들이 모두 쉽게 기록이 된다.
  • 기록을 재생하여 리플레이 시스템을 구현할 수 있다.

➔ 커맨드 패턴 개요

  • Clint : 커맨드 객체를 생성, 그 커맨드가 어떤 Receiver와 연결될지를 결정한다.

  • Invoker (호출자) : 커맨드를 받아서 실행한다.

  • Command (커맨드) : 실행될 모든 명령에 대한 인터페이스

  • Receiver (수신자) : 실제로 작업을 수행할 객체.

장점

  • 분리 : 실행하는 객체와 호출하는 개체가 분리된다.

  • 명령하는 것을 큐에 넣어서 리플레이, 매크로, 명령 큐 등을 구현할 수 있다.

단점

  • 각각의 명령을 하나의 클래스로 구현해야되서 좀 복잡하다.



➔ Command.cs

public abstract class Command
{
    public abstract void Execute();
}



➔ Invoker.cs

class Invoker : MonoBehaviour
{
    private bool _isRecording;
    private bool _isReplaying;
    private float _replayTime;
    private float _recordingTime;
    private SortedList<float, Command> _recordedCommands = 
        new SortedList<float, Command>();

    public void ExecuteCommand(Command command)
    {
        command.Execute();
        
        if (_isRecording) 
            _recordedCommands.Add(_recordingTime, command);
        
        Debug.Log("Recorded Time: " + _recordingTime);
        Debug.Log("Recorded Command: " + command);
    }

    public void Record()
    {
        _recordingTime = 0.0f;
        _isRecording = true;
    }

    public void Replay()
    {
        _replayTime = 0.0f;
        _isReplaying = true;
        
        if (_recordedCommands.Count <= 0)
            Debug.LogError("No commands to replay!");
        
        _recordedCommands.Reverse();
    }
    
    void FixedUpdate()
    {
        if (_isRecording) 
            _recordingTime += Time.fixedDeltaTime;
        
        if (_isReplaying)
        {
            _replayTime += Time.fixedDeltaTime;

            if (_recordedCommands.Any()) 
            {
                if (Mathf.Approximately(
                    _replayTime, _recordedCommands.Keys[0])) {

                    Debug.Log("Replay Time: " + _replayTime);
                    Debug.Log("Replay Command: " + 
                                _recordedCommands.Values[0]);
                    
                    _recordedCommands.Values[0].Execute();
                    _recordedCommands.RemoveAt(0);
                }
            }
            else
            {
                _isReplaying = false;
            }
        }
    }
}



➔ TrunLeft.cs

public class TurnLeft : Command
{
    private CharacterController _controller;

    public TurnLeft(CharacterController controller)
    {
        _controller = controller;
    }

    public override void Execute()
    {
        _controller.Turn(CharacterController.Direction.Left);
    }
}



➔ TrunRight.cs

public class TurnRight : Command
{
    private CharacterController _controller;

    public TurnRight(CharacterController controller)
    {
        _controller = controller;
    }

    public override void Execute()
    {
        _controller.Turn(CharacterController.Direction.Right);
    }
}
profile
No Easy Day

0개의 댓글