유니티/Script

Unity Edit Mode에서 오브젝트 부서지는 효과 만들기

어나더원 2025. 2. 7. 16:19

Unity 오브젝트 부서지는 효과 — 개발과 연구

 

Unity 오브젝트 부서지는 효과

Nvidia Blast - News & General Discussion - Unity Discussions 해당 효과는 위의 글에서 bocs라는 분이 nvidia blast를 unity에서 쉽게 쓸 수 있도록만든 외부 라이브러리를 사용했습니다. 구현  일단 위의 패키지를

gamecoke.tistory.com

이번 글은 위 글에 있는 Blast 패키지와 FractureTool.cs 스크립트가 필요합니다.

또한, UI Toolkit이 필요한데 이건 아마 2021.3 이상 유니티 버전에 기본으로 깔려있을겁니다.

 

 

Play Mode에서 실시간으로 만드는 것보다

미리 만들어 놓고 설정하는 게 더 편할 것 같아서 Edit Mode도 만들었습니다.


 

구현

Fracture Generator.unitypackage
0.00MB

 

이 파일은 UI Toolkit으로 만든, 위 움짤에서 보이는 Fracture Generater 에디터의 uxml들입니다.

해당 파일을 Import 해서 아무대나 푸신 다음

 

FractureInEditMode.cs

using System.Collections.Generic;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;

public class FractureInEditMode : EditorWindow
{
    private const int WINDOW_PRIORITY = 0;
    
    private UnsignedIntegerField    breakForceField;
    private FloatField              densityField;
    private SliderInt               siteCountSlider;
    private Button                  generateButton;

    private List<FractureData>      fractureDataList;
    
    private VisualTreeAsset         visualTree, dataVisualTree;
    private ListView                fracturableObjectListView;
    
    [MenuItem("Tools/Fracture Editor", priority = WINDOW_PRIORITY)]
    public static void ShowWindow()
    {
        GetWindow<FractureInEditMode>("Fracture Editor");
    }

    private void Awake()
    {
        fractureDataList = new List<FractureData>();
        visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("!! FractureEditor.uxml의 위치 !!");
        dataVisualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("!! FractureData.uxml의 위치 !!");
    }

    private void CreateGUI()
    {
        visualTree.CloneTree(rootVisualElement);
        fracturableObjectListView = rootVisualElement.Q<ListView>("fracturableObjectListView");
        
        AddListViewItem(fracturableObjectListView, dataVisualTree);
        
        breakForceField = rootVisualElement.Q<UnsignedIntegerField>("breakForceField");
        densityField = rootVisualElement.Q<FloatField>("densityField");
        siteCountSlider = rootVisualElement.Q<SliderInt>("siteCountSlider");
        generateButton = rootVisualElement.Q<Button>("generateButton");
        generateButton.clicked += GenerateFracture; 
    }

    private void AddListViewItem(ListView fracturableObjectListView, VisualTreeAsset dataVisualTree)
    {
        fracturableObjectListView.makeItem = dataVisualTree.CloneTree;
        fracturableObjectListView.bindItem = BindItem;
        fracturableObjectListView.itemsSource = fractureDataList;
        fracturableObjectListView.RefreshItems();
    }
    
    private void BindItem(VisualElement element, int idx)
    {
        fractureDataList[idx] = fractureDataList[idx] ?? CreateInstance<FractureData>();
        
        var data = fractureDataList[idx];
        var fracturableObjectField =    element.Q<ObjectField>("fracturableObject");
        var insideMaterialField =       element.Q<ObjectField>("insideMaterial");
        var outsideMaterialField =      element.Q<ObjectField>("outsideMaterial");
        var haveIndividualSettings =    element.Q<Toggle>("haveIndividualSettings");
        var individualSettingsWnd =     element.Q<VisualElement>("individualSettings");
        
        fracturableObjectField.RegisterValueChangedCallback((obj) => data.fracturableObject = obj.newValue as GameObject);
        insideMaterialField.RegisterValueChangedCallback((obj) => data.insideMaterial = obj.newValue as Material);
        outsideMaterialField.RegisterValueChangedCallback((obj) => data.outsideMaterial = obj.newValue as Material);
        haveIndividualSettings.RegisterValueChangedCallback((evt) =>
        { 
            data.haveIndividualSettings = evt.newValue;
            ActiveIndividualSettingsWindow(evt, individualSettingsWnd, idx);
        });
         
        fracturableObjectField.value =  data.fracturableObject;
        insideMaterialField.value =     data.insideMaterial;
        outsideMaterialField.value =    data.outsideMaterial;
        haveIndividualSettings.value =  data.haveIndividualSettings;
    }
    
    private void ActiveIndividualSettingsWindow(ChangeEvent<bool> evt, VisualElement individualSettingsWnd, int idx)
    {
        if (evt.newValue == true)
        {
            var data = fractureDataList[idx];
            var individualBreakForceField = individualSettingsWnd.Q<UnsignedIntegerField>("breakForceField");
            var individualDensityField =    individualSettingsWnd.Q<FloatField>("densityField");
            var individualSiteCountSlider = individualSettingsWnd.Q<SliderInt>("siteCountSlider");
            
            individualBreakForceField.RegisterValueChangedCallback((value) => data.breakForce = (int)value.newValue);
            individualDensityField.RegisterValueChangedCallback((value) => data.density = value.newValue);
            individualSiteCountSlider.RegisterValueChangedCallback((value) => data.siteCount = value.newValue);
            
            individualBreakForceField.value = (uint)data.breakForce;
            individualDensityField.value = data.density;
            individualSiteCountSlider.value = data.siteCount;
        }
        individualSettingsWnd.style.display = evt.newValue ? DisplayStyle.Flex : DisplayStyle.None;
    }
    
    private void GenerateFracture()
    {
        for (int i = 0; i < fractureDataList.Count; i++)
        {
            var data = fractureDataList[i];

            if (data.fracturableObject == null) continue;

            // 공용 속성을 사용할 경우
            if (!data.haveIndividualSettings)
            {
                data.breakForce = (int) breakForceField.value;
                data.density = densityField.value;
                data.siteCount = siteCountSlider.value;
            }
            
            data.fracturableObject.TryGetComponent(out MeshRenderer meshRenderer);
            data.fracturableObject.TryGetComponent(out Collider collider);
            meshRenderer.enabled = false;
            collider.enabled = false;
            
            var meshes = FractureTool.CreateFractureMeshes(data.fracturableObject, data, data.fracturableObject.GetComponent<MeshFilter>().sharedMesh);
            FractureTool.CreateFractureGameObjects(data.fracturableObject, data, meshes);
        }
    }
    
    private void OnDestroy()
    {
        fractureDataList = null; 
    }
}

 

위 스크립트를 만들면 되는데

여기서 주의할 점은 Awake 안에 "!! ~ !! 위치" 라고 적힌 곳에

FractureEditor.uxml, FractureData.uxml 경로를 설정해 줘야합니다.

예를 들어, Assets/Scripts/FractureEditor.xml 이 있으면 이 경로를 그대로 적어주면 됩니다.

 


사용법

사용법은 Play Mode에서 생성했던 방식과 다릅니다.

 

 

우선 Tools에 Fracture Editor를 클릭합니다.

 

 

그러면 Fractures가 접혀있을 수도 있기 때문에 펼칩니다.

그리고 오른쪽 중간에 + 버튼을 누르면 아래 Item이 추가됩니다.

 

 

여기서 조각날 GameObject에 조각내고 싶은 오브젝트를 넣으면 되는데

주의할 점은 오브젝트 MeshFilter에 Mesh가 존재해야 합니다.

 

또한, Play Mode에선 인스턴스화 된 Mesh를 사용했지만

Edit Mode에선 shared Mesh를 사용합니다.

 

안쪽 Material과 바깥쪽 Material은 넣어도 되고 안 넣어도 됩니다.

안 넣으면 안쪽 Material은 Standard Material이 들어가고

바깥쪽 Material은 조각날 GameObject의 Material이 들어갑니다.

 

개별 속성을 체크하게 되면 전체 속성에 영향을 안 받고 오브젝트 마다 설정을 다르게 할 수 있습니다.

여기서

Density는 밀도

Break Force는 Joint가 부서질 힘

Site Count는 조각 개수입니다.

 

 

대충 설정해서 Generate를 눌러주면 '조각날 GameObject' 자식에 Chunks가 추가됩니다.

 

여기서 알아둬야 할 점은

생성 후 '조각날 GameObject'의 Mesh Renderer와 Trigger는 비활성화 상태가 됩니다.

 

이렇게 생성하면 최종적으로

 

이런 식으로 분리가 되고

Play Mode와 마찬가지로 rigidbody와 trigger를 가지는 물체로 부딪히면

joint가 풀려서 부서지는 효과를 볼 수 있습니다.