Unity

[Unity] 빌딩 시스템 만들기 #2. Snap & Build

youcheachae 2024. 6. 22. 19:33

[이전 편]

https://youcheachae.tistory.com/6

 

[Unity] 빌딩 시스템 만들기 #1. UI , Connector Data

[이전 편]https://youcheachae.tistory.com/5#comment21549004 [Unity] 빌딩 시스템 만들기 #0. 구상[시스템 구성 전]1. 한글로된 빌딩 snap 기능이 없어서 검색을 해보다가 connector를 사용하여  raycast를 사용한 빌딩

youcheachae.tistory.com


1. <HousingManager.cs>

1-1. 사용할 레이어를 초기화 합니다

    [Header("===Layer===")]
    public LayerMask _buildFinishedLayer;                           // 다 지은 블럭의 layermask   
    public int _buildFinishedint;                                   // 다 지은 블럭의 layer int
    public List<Tuple<LayerMask, int>> _connectorLayer;             // 커넥터 레이어 
    public LayerMask _ConnectorWholelayer;        		  // 모든 커넥터 레이어 다 합친
    private int _placedItemLayerInt;                                // 설치 완료한 오브젝트 layer int
    
    private void F_InitLayer()
    {
        // 설치완료한 오브젝트 레이어
        _placedItemLayerInt = LayerMask.NameToLayer("PlacedItemLayer");

        // 설치완료한 블럭 레이어
        _buildFinishedint   = LayerMask.NameToLayer("BuildFinishedBlock");
        _buildFinishedLayer = LayerMask.GetMask("BuildFinishedBlock");

        // 커넥터 레이어 
        _connectorLayer = new List<Tuple<LayerMask, int>>
        {
            new Tuple <LayerMask , int>( LayerMask.GetMask("FloorConnectorLayer") , LayerMask.NameToLayer("FloorConnectorLayer") ),         // temp floor 레이어
            new Tuple <LayerMask , int>( LayerMask.GetMask("CellingConnectorLayer") , LayerMask.NameToLayer("CellingConnectorLayer") ),     // temp celling 레이어
            new Tuple <LayerMask , int>( LayerMask.GetMask("WallConnectorLayer") , LayerMask.NameToLayer("WallConnectorLayer") ),           // temp wall 레이어
            new Tuple <LayerMask , int>( LayerMask.GetMask("WallConnectorLayer") , LayerMask.NameToLayer("WallConnectorLayer") ),           // temp wall 레이어 ( destory 도구 위해서 )
            
        };

        // 커넥터 다 합친 레이어 
        _ConnectorWholelayer = _connectorLayer[0].Item1 | _connectorLayer[1].Item1 | _connectorLayer[2].Item1;

    }

 

1-1. HousingUiManager에서 F_BuildingMovement()함수를 호출 후 동작을 위한 코루틴이 실행됩니다.

[System.Serializable]
public enum SelectedBuildType
{
    Floor,
    Celling,
    Wall,
    Door,
    Window,
    RepairTools
}

public class HousingManager : MonoBehaviour
{
    [Header("===Transform===")]
    public GameObject _playerCamera;
    
    [HideInInspector] public GameObject _tempObject;                 // 임시 오브젝트
    [HideInInspector] public Transform _modelTransform;              // 모델 오브젝트 
    [HideInInspector] public Transform _colliderGroupTrasform;       // 콜라이더 부모 오브젝트 
    [HideInInspector] Transform _otherConnectorTr;                   // 충돌한 다른 오브젝트 (커넥터)
    [HideInInspector] private SelectedBuildType _SelectBuildType;	 // 현재 type
        
    private int _snapObjectTypeIdx;                            		// typeidx
    private int _snapObjectDetailIdx;				        // detailIdx
    private List<Material> _snapOrimaterial;				// 원래 material 저장 list
    
    [Header("State")]
    [SerializeField] bool _isTempValidPosition;             // 임시 오브젝트가 지어질 수 있는지
    [SerializeField] bool _isntColliderPlacedItem;          // 설치완료된 오브젝트와 겹치는지 ?
    
    [Header("Prefab")]
    [SerializeField] public List<List<GameObject>> _bundleBulingPrefab;
    [SerializeField] private List<GameObject> _floorList;
    [SerializeField] private List<GameObject> _cellingList;
    [SerializeField] private List<GameObject> _wallList;
    [SerializeField] private List<GameObject> _doorList;
    [SerializeField] private List<GameObject> _windowList;
    
    [Header("===LayerMask===")]
    [SerializeField] LayerMask _currTempLayer;              // 현재 (temp 블럭이 감지할) 레이어 
    
    [Header("===Material===")]
    [SerializeField] private Material _greenMaterial;
    [SerializeField] private List<Material> _oriMaterialList;
    [SerializeField] private Material _nowBuildMaterial;

    private void Awake()
    {
        // 1. 초기화                      
        _bundleBulingPrefab = new List<List<GameObject>> // 각 block 프리팹 List를 하나의 List로 묶기
        {
            _floorList,
            _cellingList,
            _wallList,
            _doorList,
            _windowList
        };
        
     }
     
    public void F_BuildingMovement(int v_type , int v_detail)
    {
        // 0. Ui상 잘못된 idx가 넘어왔을 때 
        if (v_type < 0 && v_detail < 0)
            return;

        // 1. idx 저장
        _snapObjectTypeIdx     = v_type;
        _snapObjectDetailIdx   = v_detail;

        // 2. 임시 오브젝트 확인
        if (_tempObject != null)
            Destroy(_tempObject);
        _tempObject = null;

        // 3-1. Material 리스트 초기화
        _oriMaterialList.Clear();
        // 3-1. 위치 state 초기화
        _isTempValidPosition = true;
        // 3-2. 다른오브젝트와 겹치는지에 대한 state 초기화
        _isntColliderPlacedItem = false;

        // 4. 동작 시작 
        StopAllCoroutines();
        StartCoroutine(F_TempBuild());
    }

}

 

 

1-3. F_TempBuild() 코루틴

  IEnumerator F_TempBuild()
  {    

      // 0. index에 해당하는 게임 오브젝트 return
      GameObject _currBuild = F_SetTypeReturnObj(_snapObjectTypeIdx, _snapObjectDetailIdx);
      
      // 0.1. 내 블럭 타입에 따라 검사할 layer
      F_SettingCurrLayer(_SelectBuildType);

      while (true)
      {
          // 수리 & 파괴도구 일 때
          if (_SelectBuildType == SelectedBuildType.RepairTools)
          {
              F_RepairAndDestroyTool( _housingManager_Detailidx , _currTempLayer );
          }
          // 그 외 (건축 type 일 때)
          else
          {
              // 1. index에 해당하는 게임오브젝트 생성 , tempObjectbuilding 오브젝트 생성 
              F_CreateTempPrefab(_currBuild);

              // 2. 생성한 오브젝트를 snap , tempObjectBuilding 오브젝트 넘기기 
              F_OtherBuildBlockBuild();
          }

          // update 효과 
          yield return new WaitForSeconds(0.02f);
      }
  }

	// SelectbuildType지정, 해당하는 prefab return
    public GameObject F_SetTypeReturnObj(int v_type, int v_detail)
    {
        _SelectBuildType = (SelectedBuildType)v_type;
        return _bundleBulingPrefab[v_type][v_detail];
    }
    
    // type에 따른 Layer 설정 
    private void F_SettingCurrLayer(SelectedBuildType v_type)
    {
        switch (v_type)
        {
            case SelectedBuildType.Floor:
                _currTempLayer = BuildMaster.Instance._connectorLayer[0].Item1;                          // floor 레이어
                break;
            case SelectedBuildType.Celling:
                _currTempLayer = BuildMaster.Instance._connectorLayer[1].Item1;                          // celling 레이어
                break;
            case SelectedBuildType.Wall:                                                                 // window, door, window는 같은 wall 레이어 사용 
            case SelectedBuildType.Door:
            case SelectedBuildType.Window:
                _currTempLayer = BuildMaster.Instance._connectorLayer[2].Item1;                          // wall 레이어
                break;
            case SelectedBuildType.RepairTools:                                                          // repair 툴 일 때 
                _currTempLayer = BuildMaster.Instance._buildFinishedLayer;                               // buildFinished
                break;
        }
    }

0️⃣ Update 효과를 주기위해 코루틴 내부에서 0.02f만큼 지연을 줍니다.

1️⃣type Idx와 detail Idx를 사용해서 SelectBuildType를 지정후 prefabBundle에 접근하여 오브젝트를 return합니다.

2️⃣type에 따라  Raycast할 layer을 설정합니다.

3️⃣타입에 따라 동작이 나뉩니다

       3-1. Repair 도구일 때 

               : 수리 & 파괴 동작

        3-2. 그 외 타입일 때 

               : 블럭을 snap

               : 블럭을 build

 


 

[Snap]

    private void F_CreateTempPrefab(GameObject v_temp)
    {
        // 실행조건
        // temp오브젝트가 null이되면 바로 생성됨!
        if (_tempObject == null)
        {
            // 1. 생성 & 100f,100f,100f는 임시위치 
            _tempObject = Instantiate(v_temp, new Vector3(100f, 100f, 100f), Quaternion.identity);

            // 2. model Transform & collider group Transform 
            _modelTransform         = _tempObject.transform.GetChild(0);
            _colliderGroupTrasform  = _tempObject.transform.GetChild(1);

            // 2-1. 오브젝트 하위 collider 오브젝트의 하위 콜라이더를 trigger 
            F_ColliderTriggerOnOff(_colliderGroupTrasform, true);

            // 3. 원래 material 저장
            _oriMaterialList.Clear();
            for (int i = 0; i < _modelTransform.childCount; i++) 
            {
                _oriMaterialList.Add(_modelTransform.GetChild(i).GetComponent<MeshRenderer>().material); 
            }

            // 4. modeld의 Material 바꾸기
            F_ChangeMaterial(_modelTransform, _greenMaterial);
        }
    }
    
    public void F_OtherBuildBlockBuild()
    {
        // 1. 해당 블럭이랑 같은 Layer만 raycast
        F_Raycast(_currTempLayer);

        // 2. temp 오브젝트의 콜라이더 검사
        F_CheckCollision(_currTempLayer);

        // 3. 우클릭 시 설치
        if (Input.GetMouseButtonDown(0))
            F_FinishBuild();
    }
    
    // 블럭 하위 colliderGroup의 자식들 collider on / off
    public void F_ColliderTriggerOnOff(Transform v_trs, bool v_flag)
    {
        for (int i = 0; i < v_trs.childCount; i++) 
        {
            v_trs.GetChild(i).GetComponent<Collider>().isTrigger = v_flag;
        }
    }
    
    // 블럭 하위 material을 바꾸기 
    public void F_ChangeMaterial(Transform v_pa, Material material)
    {
        foreach (MeshRenderer msr in v_pa.GetComponentsInChildren<MeshRenderer>())
        {
            msr.material = material;
        }
    }

 

F_CreateTempPrefab()

0️⃣_tempObject가 null 일 때만 _tempObject를 생성합니다.

       1️⃣prefab 하위의 model의 Transfrom과, collider의 Transform을 담아놓습니다.

하위에 Model과 collider Group 이 존재

            : 최상위 Empty 오브젝트 하위에 Model와 Collider Group 의 Empty 오브젝트가 존재합니다.

                      -  Model

                              : 하위에 MeshRenderer과 MeshFilter가 부착되어있는 자식 Object가 있습니다.

                      - collider Group

                             : mesh collider을 사용하지 않고 Primitive 콜라이더를 사용하기 위해서 존재합니다.

                             : 하위에 Primitive 콜라이더가 부착되어있는 자식 Object 가 있습니다.

       2️⃣ collider Group 하위의 오브젝트의 기본 layer은 BuildFinished 레이어 입니다.        

             : snap 시 플레이어와 충돌하지 않기 위해서 collider의 onTrigger을 On 합니다. 

Layer가 BuildFinishedBlock

        3️⃣ Model 하위의 MeshRenderer의 Material을 List에 저장한 후, 프리팹을 초록색으로 바꿉니다. 

              : 임시 build 상태를 나타내기 위해서 초록색으로 변환합니다. 

Material을 초록색으로 바꿈

✅F_Raycast()

private void F_Raycast(LayerMask v_layer)
{
    // 1. 넘어온 Layer'만' rayCast
    RaycastHit _hit;

    // 2. raycast 되면 -> 임시 오브젝트를 그 위치로 옮기기 
    if (Physics.Raycast
        (_playerCamera.transform.position, 
            _playerCamera.transform.forward * 10, out _hit, 5f, v_layer)) // 타입 : LayerMask
    {
        _tempObject.transform.position = _hit.point;
    }
    
}

1️⃣ 플레이어 카메라 위치에서, 플레이어 카메라 위치 앞쪽으로 , 최대거리 5f만큼 v_layer만 검사합니다. 

2️⃣ raycast가 충돌되면 , 임시 오브젝트의위치를 충돌된 위치로 이동합니다.

 

 

✅F_CheckCollision()

    private void F_CheckCollision(LayerMask v_layer)
    {
        // 1. 콜라이더 검사 
        Collider[] _coll = Physics.OverlapSphere(_tempObject.transform.position, 1f, v_layer);    // 타입 : LayerMask

        // 2. 검사되면 -> 오브젝트 Snap
        if (_coll.Length > 0)
            F_Snap(_coll);
        // 2-1. 검사되지 않으면 , 올바른 위치에 있지 않음 
        else
            _isTempValidPosition = false;

    }

1️⃣블럭의 중심에서 OverlapSphere로 콜라이더를 검사합니다.

     : 콜라이더가 검출되면 ? Snap 동작합니다.

2️⃣검사되지 않으면 ? 

    : 올바른 위치가 아닙니다.

 

✅F_Snap()

    private void F_Snap(Collider[] v_coll)
    {
        // 0. 다른 커넥터? -> 배열에 처음으로 들어온 collider
        _otherConnectorTr = v_coll[0].transform;

        // 1. 타입이 wall 일때는 회전 
        if (_snapSelectBuildType == SelectedBuildType.Wall || _snapSelectBuildType == SelectedBuildType.Window
            || _snapSelectBuildType == SelectedBuildType.Door)
        {
            // 내 temp 블럭 회전 += 접촉한 커넥터의 회전
            Quaternion qu = _snapTempObject.transform.rotation;
            qu.eulerAngles = new Vector3(qu.eulerAngles.x, _otherConnectorTr.eulerAngles.y, qu.eulerAngles.z);
            _snapTempObject.transform.rotation = qu;
        }

        // 2. Snap!! 
        _tempObject.transform.position
             = _otherConnectorTr.position;
             
        // 3. 설치가능 
        _isTempValidPosition = true;
    }

1️⃣ 충돌한 다른 오브젝트 즉 , 커넥터를 저장합니다. 매개변수로 넘어온 collider 배열에서 맨 처음 오브젝트입니다.

2️⃣ 현재 type이 wall , window , door 이면 충돌한 다른 오브젝트 즉 커넥터의 회전을 적용합니다.

3️⃣ 현재 임시오브젝트 ( _tempObject )를 충돌한 커넥터의 위치로 이동합니다. ( Snap 동작)

커넥터 회전에 따라 블럭이 회전합니다.

 

 

✅F_BuildTemp()

: 우클릭시 동작합니다.

 private void F_BuildTemp()
 {
     // 0. 올바른 위치에 있으면, 다른 오브젝트랑 충돌한 상태가 아니면 
    if (_isTempValidPosition != true)
    	return;
        
     // 1. 설치   
     if (_tempObject != null)
     {
         // 0. 생성
         GameObject _nowbuild = Instantiate(F_SetTypeReturnObj(_snapObjectTypeIdx, _snapObjectDetailIdx),
             _tempObject.transform.position, _tempObject.transform.rotation );

         // 1. destory
         Destroy(_tempObject);
         _tempObject = null;

         // 3. model의 material 변경 
         Transform _nowBuildObjModel = _nowbuild.transform.GetChild(0);
         for (int i = 0; i < _nowBuildObjModel.childCount; i++)
             _nowBuildObjModel.GetChild(i).GetComponent<MeshRenderer>().material = _oriMaterialList[i];

         // 4-1. collider group 오브젝트의 하위 콜라이더를 trigger Off
         F_ColliderTriggerOnOff(_nowbuild.transform.GetChild(1), false);

         // 5. 커넥터 지정 
         F_CreateConnector( _nowBuildBlock.transform );

         // 6. 그 자리에 원래있던 커넥터 destory
         Destroy(_otherConnectorTr.gameObject);
     }
 }

0️⃣ 올바른 위치에 있으면

        1️⃣ 현재 typeidx와 detailidx에 맞는 오브젝트를 Instantiate 합니다.  

        2️⃣ _tempObject를 null로 지정합니다 

             : F_CreateTempPrefab(GameObject)에서 _tempObject를 매프레임 검사하는데, null이 되는 순간 함수내 조건이                  true가 되면서 _tempObject를 생성합니다.

        3️⃣ 현재 블럭에 해당하는 커넥터를 생성합니다. (Destroy부분에서 설명예정)

        4️⃣ F_Snap 부분에서 저장했던 커넥터 오브젝트를 destory 합니다

우클릭 시 블럭을 설치합니다.

 


https://github.com/churush912837465

 

churush912837465 - Overview

churush912837465 has 4 repositories available. Follow their code on GitHub.

github.com