위에 조건처럼 [어떤 동작을해도 같은 목적을 취함 + 동작이 다름] 이라는 조건하에 전략패턴커맨드패턴의 디자인패턴을 자주 사용할 것이다. 하지만 UI의 버튼이나 규모가 작은 경우에는 전략패턴을 구조로 설계하기에 리소스 낭비적인 부분이 있다. 

 

이런 경우에는 <매핑 테이블 활용>을 하여 각 기능을 정의한뒤 인자값을 다르게 설정하는 것이다. 

 

using UnityEngine;
using UnityEngine.UI;
using TMPro;
using System.Collections.Generic;
using Unity.Burst.Intrinsics;

public class UpgradesController : MonoBehaviour 
{
    [SerializeField] private SlimeTowerStatUpgradeData[] _upgradeDatas;
    [SerializeField] private Button[] buttons;

    private Dictionary<Button, TowerGrade> upgradeButtonMappings;

    private void Awake()
    {
        upgradeButtonMappings = new Dictionary<Button, TowerGrade>
        {
            { buttons[0], TowerGrade.Common},
            { buttons[1], TowerGrade.Normal},
            { buttons[2], TowerGrade.Epic},            
        };

        foreach(KeyValuePair<Button, TowerGrade> pair in upgradeButtonMappings)
        {
            pair.Key.onClick.AddListener(() => UpgradeTowerByGrade(pair.Value));
        }
    }

    //버튼 누르면 해당 등급에 따라 강화 
    public void UpgradeTowerByGrade(TowerGrade grade)
    {
        foreach (var data in _upgradeDatas)
        {
            if (data.Grade == grade)
            {
                data.OnUpgrade();
                break;
            }
        }
    }
}

위 예제 코드는 버튼이 3개가 있고 , 해당 버튼들은 해당 등급의 유닛들을 강화해주는 기능이있다.

목적은 강화기능이기에 등급이라는 인자값을 받아서 적용시켜주어야한다. 

 

초기화시 사용할 데이터를 Inspector를 통해서 초기화한뒤(스크립트 코드로 적용시켜도 됨)

버튼의 onClick.AddListener로 람다식을 이벤트로 초기화 하여 사용하는 방식을 취한다.

 


이런식으로 테이블매핑을 한뒤 각 기능에 맞춰 이벤트를 적용시켜주면 전략패턴을 사용하지않고

간단히 사용 할 수 있다.

파일 경로명을 인자값으로 받아서 CSV데이터를 읽어 파싱한 후  List<Vector3>로 반환한다.

반환값이 T가 아닌 고정되어있어 범용성이 떨어지는 메서드이다. 나중에 Refactoring이 필요해보인다. 

 

Stage Way Point는 Vector3를 저장하는 CSV 외부데이터이다.

 

  public static List<Vector3> LoadStageWayPointFromCSV(string fileName)
  {
      // Resources 폴더에서 파일 로드
      TextAsset csvFile = Resources.Load<TextAsset>(fileName);

      if (csvFile == null)
      {
          Debug.LogError($"File {fileName} not found in Resources folder.");
          return null;
      }

      // 데이터를 한 줄씩 분리
      string[] lines = csvFile.text.Split('\n');
      List<Vector3> wayPointList = new List<Vector3>();

      //첫번쨰 줄은 헤더이기에 제외 
      for (int i = 1; i < lines.Length; i++)
      {

          string line = lines[i].Trim(); // 공백 제거
          if (string.IsNullOrEmpty(line)) continue; // 빈 줄 무시

          string[] values = line.Split(',');

          if (int.TryParse(values[0], out int x) &&
              int.TryParse(values[1], out int y) &&
              int.TryParse(values[2], out int z))
          {
              wayPointList.Add(new Vector3Int(x, y, z));
          }
      }

      return wayPointList;
  }

비록 간단한 내용이지만, 생각밖으로 놓치고 갈 수도 있을것 같아 정리를 하였다.

 

0.생명주기 함수(Update,Start)들을 사용하지 않으면 무조건 지워라

이 함수들은 [리플렉션]이란 것때문에 호출만 하고 있어도 동작을 하여 처리비용을 계속 잡아먹기 때문이다. 

 

p.s 리플렉션

리플렉션은 컴파일 시에 알 수 없었던 타입이나 멤버들을 찾아내고 사용할 수 있게 해주는 메커니즘이다.

리플렉션 정보는 해당 블로그 글을 참고하자.

https://tsyang.tistory.com/56

 

C# - 리플렉션 (Reflection)

리플렉션 리플렉션은 컴파일 시에 알 수 없었던 타입이나 멤버들을 찾아내고 사용할 수 있게 해주는 메커니즘이다. 그러나 다음의 주요한 단점이 존재한다. 리플렉션을 사용하면 컴파일 시에

tsyang.tistory.com

 

1. 빌드시에 Debug.Log()를 지우고 사용하기

빌드시에도 코드영역에 Debug.Log()가 남아있는경우 여전히 처리비용이 들며 , 해킹의 여지도 있게된다.

왜냐하면 개발시에 디버깅용으로 넣은 Debug.Log()로 데이터를 파악하기떄문이다.

 

2.LinQ함수 남용 금지

Linq 패키지로 구현이 가능한건 for과if문으로 구현이 가능하기 때문이며 , Linq는 생각밖으로 처리비용이 높기때문이다.

(Linq와 for문 루프 돌린 성능 비교 그래프 참조 필요함)

 

3.코루틴 사용시 yield return new WaitForSeconds(); 사용 지양하기

사실 코루틴은 반복하게 하는 메서드이지만 new라는 키워드로 인해서 결국 반복적으로 객체를 생성하고

코루틴이 사용이 완료되면 해당 데이터는 가비지가 되어 결국 최적화에 영향을 끼친다.

WaitForSeconds updateTime = new WaitForSeconds(1.0f);

이런식으로 class내에 멤버변수로 선언하고 사용하는것이 올바르다. 

 

4.오브젝트 풀링시 대량의 오브젝트 생성시 자식오브젝트로 생성하지 마라

하이어라키에 오브젝트 풀링을 정리하겠다고 부모오브젝트를 생성하고 그 밑에 만드는데

결국 하이어라키는 계층구조(tree)이기떄문에 나중에 위치가 이동되거나 할때 문제가 발생하기 때문이다. 

이번 개인 프로젝트 진행중에 Player,Enemy,Item의 데이터를 ScriptableObject 로 관리 해보았다.

또한 저장/불러오기 기능을 ScriptableObject 로 구현해보기 위해서였다.

 

하지만 Player,Enemy의 Status 데이터들은 계속 변동 되는 값이였기에 나중에 게임을 다시 시작하거나 하면 데이터가 유지는 되는데 아이템을 장착하지 않았는데 공격력이 적용 되어 있거나 그랬다.

 

Item처럼 불변되지 않는값에 ScriptableObject 를 사용하여 데이터를 호출하는 목적으로만 사용하는것이 좋은것 같다,. 

[문제점]
Enemy Idle Class에서 Enemy 의 변수인 Data(EnemyData 스크립타블오브젝트)에 Collider변수를 저장한뒤
Enemy Chase Class에서 SO를 호출하니 null이 반환되는 건

 

[첫번쨰 대처]

1. Collider -> GameObject로 타입으로 수정한뒤  Collider의 정보가 필요할때 TryGetComponet를 이용해서 호출하였지만 되지 않았음 

 

해당 대처를 한 이유 : 

ScriptableObject에서 Collider와 같은 Unity 컴포넌트 타입은 일반적인 값이나 데이터처럼 직접 저장하거나 유지할 수 없습니다.  ScriptableObject는 주로 데이터만 저장하기 위한 용도로 설계목적이기 때문

 

디버깅중 찾은 원인은 Enemy가 하나만 있는경우 상관이 없지만
Enemy가 여러명이 되면서 해당 Data에 접근해서 계속 Data를 바꾸게 로직을 작성함

 

즉, ScriptableObject는 참조형식(Class) 이기에 A Class에서 사용중이다가 B Class값을 변경하게되면 A Class에서 사용하던 데이터가 사라지기 때문이였다. 

Class => 참조형식이다.

[해결방법]

Enemy Data(스크립타블오브젝트)에서는 Data 호출만하게 하고

Enemy의 Class로직을 담당하는곳에 따로 필요한 변수(Target)를 선언하여 사용하기로 함 

 

MVC 패턴이란 Model - View - Controller의 약자이며 , Unity 에서 개임 개발시 주로  UI에 사용된다.

 

Model : 데이터를 의미하며 , DB를 만들어 데이터를 보관하는 목적이다.

데이터는 엑셀을 이용해서 외부에서 파싱해서 사용 할 수 있다.

데이터들은 ID 라는 int 정수값을 이용해서 사용 하는것이 매우 유리하다.

 

View : 유저들에게 눈으로 보여지는 것을 의미하며 유니티로는 Canvas의 UI를 의미하며,

UI 스크립트에는 로직이 아닌 오로지 출력이 필요하다. 출력이 필요한 데이터는 컨트롤러를 통해서 전달 받아야한다.

 

Controller : Model과 View의 중간 다리 역할이라고 보면된다. 즉, Manager와 역할이 비슷하며

해당 영역에 로직을 작성하여 Model과 View를 Update를 해주는 역할이다. 

https://01149.tistory.com/127

 

설계 :: 디자인 패턴 - 전략패턴,프로토타입패턴,컴포지트패턴,퍼사드패턴

전략 패턴 ( Strategy Pattern )더보기목적을 이루기 위해 정해진 공통의 행동이 있지만 그 안에서 공통의 행동전략들을 만들어 접근하는 패턴 예시 1)A가 공항을 가기야된다라는 목적 1.차를 타고 

01149.tistory.com

 

객체가 경사로를 이동할때 위로 뿅하고 튀어오르는 현상이 있을거다. 분명 나는 인풋으로 x,z축 데이터만 넣었는데 왜 경사로에서 튀어오르는지 의문일 것이다.

 

우리가 진행하는 방향이 moveDir , 경사로에 올라갈때 transform.up 값이 추가된다.

 

y값을 보정으로 하고있어도 경사로의 콜라이더와 플레이어의 콜라이더가 충돌이 일어나 생기는 힘이기 때문에 위로 튀어오를수 밖에 없을것이다.

 

이럴떄는 경사도의 법선벡터를 이용하여 투영벡터를 계산해서 진행방향을 알아내야한다.

 

우선 밝고있는 지형이 경사도인지 체크해야된다.

경사도를 오르는 객체의 아랫방향(Vector3.down)으로 ray를 쏴서 해당 경사면의 각도를 알아야한다.

 

p.s 경사도의 각도를 알아야하는 이유

더보기

경사면의 각도를 알아야 하는 이유는 이동오브젝트가 여기 경사면을 넘을수 있는지 없는지를 체크하기 위해서다.

즉, 완만한 경사면 오를수 있을것이고 급한경사는 못오르는 maxSlopeAngle 변수를 설정해서 컨트롤 해 줄수 있게 해야된다.

그러기위해서 경사면의 노말벡터(법선벡터,Slopehit.Normal)  윗 방향의 벡터(Vector3.up) 사이의 각도를 구한뒤에 넘어갈수 있는 각도인지 체크한다. 

 

넘어 갈수 있게되면 해당 경사면의 진행 방향을 알아야된다. 

ProjectOnPlane() : 평면에서의 투영벡터를 구하는 메서드

ProjectOnPlane() 메서드를 사용하여 내가 이동하는 방향벡터 movediretion경사도의 법선벡터 slopeHit.normal투영 벡터를 구해야만 해당 경사도에서의 이동 방향을 구 할수 있게된다. 

출처 : https://code-piggy.tistory.com/entry/%EC%9C%A0%EB%8B%88%ED%8B%B0-Vector3ProjectOnPlane?pidx=0

그림으로 간단히 표현한 블로그가 있어 사진을 퍼왔다.

 

위의 설명한 이론을 아래 코드로 구현해보았다.

   
   public class PlayerController : MonoBehaviour
{
 private Rigidbody rigid;
  public PlayerStatus status;
  
  
    [Header("Slope Handling")] //경사도에 올라갈시 위로 솟아오르는 현상 제어
    public float maxSlopeAngle; //해당 경사도 보다 작은경우 캐릭터가 올라갈수 있게 설정
    private RaycastHit slopeHit; //캐릭터 밑 경사도의 법선벡터 호출용으로 사용되는 변수

  
   [Header("Movement")]
    private Vector2 curMoveInput;
    private Vector3 moveDirection;
    public Vector3 ExtraDir; //외부에서 힘을 가할떄 x,z축 이동방향값을 보정해주는 변수
    
   [Header("IsGrounded")]
    public float RayDistance;
    public Transform GroundPivot;
    public LayerMask GroundMask;

  //y축 보정을 스크립트로 중력 추가 보정 함수
  private void ApplyCustomGravity()
  {
      // 중력을 수동으로 추가하여 y축 낙하를 더 강하게 설정
      Vector3 gravity = Physics.gravity * 3.0f;
      rigid.AddForce(gravity, ForceMode.Acceleration);
  }


//캐릭터의 이동을 담당하는 함수
 public void Move()
 {
 	//z축(forward)와 x축(right)방향 입력값을 받음
     moveDirection = curMoveInput.y * transform.forward + curMoveInput.x * transform.right;
     //점프를 하지 않는 이상 y축 값은 변동이 없기에 y축 보정
     float velocity_Y = rigid.velocity.y;
	
    //경사도 기울기 체크
     if (OnSlope())
     {
     	//경사로에 있기 때문에 경사로의 객체 속도 보정
         moveDirection = GetSlopeMoveDirection();
         //경사로에 진행방향에 맞춰 y축도 속도를 보정해준다.
         velocity_Y = moveDirection.y * status.CurSpeed;
     }

     Vector3 moveVelocity = moveDirection * status.CurSpeed;
     rigid.velocity = new Vector3(moveVelocity.x, velocity_Y, moveVelocity.z) ;
 }

//플레이어 객체가 경사도에 있는지 체크 및 이동 가능한지 확인하는 함수
 private bool OnSlope() 
 {
     Ray ray = new Ray(GroundPivot.position, Vector3.down);
     
     //0.3f는 경사도에 있기에 추가로 더 길게 쏜것
     if (Physics.Raycast(ray, out slopeHit, 0.3f, GroundMask))
     {
     	
         // Vector3.Angle : 두개의 벡터 사이의 각도값을 구하는 함수
         /*해당 로직은 월드 y축(uP) 방향과 경사로의 법선벡터(slopehit) 사이의 각도를 받아온다.*/
         float angle = Vector3.Angle(Vector3.up, slopeHit.normal);
         return angle <= maxSlopeAngle && angle != 0;
     }

     return false;
 }

//경사로에 있기 때문에 경사로의 객체 속도를 보정해주는 함수
 private Vector3 GetSlopeMoveDirection()
 {
 	//경사로에 있는 객체의 진행 방향을 알기위해서는 투영벡터를 사용해야된다.
    //투영벡터를 사용하기 위해서 ProjectOnPlane() 메서드를 사용한다. 
     Vector3 reflectV = Vector3.ProjectOnPlane(moveDirection, slopeHit.normal).normalized;
     
     if (reflectV != Vector3.zero)
     {
         Debug.Log(reflectV);
     }
     return reflectV;
 }
}

 


해당 글을 작성하며 도움 받은 자료

https://www.youtube.com/watch?v=xCxSjgYTw9c&t=378s

https://code-piggy.tistory.com/entry/%EC%9C%A0%EB%8B%88%ED%8B%B0-Vector3ProjectOnPlane

 

Unity- Vector3.ProjectOnPlane()

Vector3.ProjectOnPlane() public static Vector3 ProjectOnPlane(Vector3 vector, Vector3 planeNormal); 파라미터 vector- 정사영하고자 하는 벡터 planeNormal - plane의 법선 벡터 반환 vector3 - vector를 평면의 법선 벡터 planeNormal에

code-piggy.tistory.com

 

+ Recent posts