샘플 프로젝트 (richardfine-scriptableobjectdemo)
소스 : https://bitbucket.org/richardfine/scriptableobjectdemo/src/default/
오디오 이벤트
추상 클래스는 다형화를 구현하는 대표적인 수단이다. 사용자인 MonoBehaviour는 인스턴스의 타입이 무엇인지에 상관없이 Play()만 호출할 뿐이다.
다형성은 OOP가 지향하는 것으로 변경에 대비해 사용자와 특정 클래스 사이를 추상 클래스나 인터페이스로 분리시키는 것이다. 기능의 변경이 발생하면 사용자의 코드는 건들지 않아도 된다. 기능 정의는 자식 클래스에서 이뤄지기 때문이다.
1
2
3
4
5
6
|
using UnityEngine;
public abstract class AudioEvent : ScriptableObject
{
public abstract void Play(AudioSource source);
}
|
AudioEvent 소스. 추상 클래스다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
using System;
using UnityEngine;
using Random = UnityEngine.Random;
[CreateAssetMenu(menuName="Audio Events/Composite")]
public class CompositeAudioEvent : AudioEvent
{
[Serializable]
public struct CompositeEntry
{
public AudioEvent Event;
public float Weight;
}
// AudioEvent를 여럿 구성한다.
public CompositeEntry[] Entries;
public override void Play(AudioSource source)
{
float totalWeight = 0;
for (int i = 0; i < Entries.Length; ++i)
totalWeight += Entries[i].Weight;
float pick = Random.Range(0, totalWeight);
for (int i = 0; i < Entries.Length; ++i)
{
if (pick > Entries[i].Weight)
{
pick -= Entries[i].Weight;
continue;
}
// AudioEvent 묶음을 재생한다.
Entries[i].Event.Play(source);
return;
}
}
}
|
CompositAudioEvent 소스. AudioEvent의 구체 클래스들 중 하나.
프로퍼티 드로워
volume과 pitch 필드는 min과 max의 범위를 정할 수 있다. 프로퍼티 드로워로 구현해보자.
Editor와 PropertyDrawer 스크립트들은 보통 각 'Editor'폴더에 위치시킨다. Editor폴더는 유일하지 않아도 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
using UnityEngine;
using Random = UnityEngine.Random;
[CreateAssetMenu(menuName="Audio Events/Simple")]
public class SimpleAudioEvent : AudioEvent
{
public AudioClip[] clips;
public RangedFloat volume;
// '[MinMaxRangeAttribute]'을 다음처럼 간단히 줄일 수 있다.
[MinMaxRange(0, 2)]
public RangedFloat pitch;
public override void Play(AudioSource source)
{
if (clips.Length == 0) return;
source.clip = clips[Random.Range(0, clips.Length)];
source.volume = Random.Range(volume.minValue, volume.maxValue);
source.pitch = Random.Range(pitch.minValue, pitch.maxValue);
source.Play();
}
}
|
pitch는 [MinMaxRange(0,2)] 와 같이 어트리뷰트가 부여됐다. 해당 어트리뷰트는 커스텀 어트리뷰트로써 임의로 작성한 어트리뷰트이다.
1
2
3
4
5
6
7
8
9
10
11
12
|
using System;
public class MinMaxRangeAttribute : Attribute
{
public MinMaxRangeAttribute(float min, float max)
{
Min = min;
Max = max;
}
public float Min { get; private set; }
public float Max { get; private set; }
}
|
어트리뷰트를 부여하더라도 System.Reflection.GetCustomAttribute()가 호출되기 전에는 어트리뷰트의 생성자가 호출되지 않는다.
1
2
3
4
5
6
7
8
|
using System;
[Serializable]
public struct RangedFloat
{
public float minValue;
public float maxValue;
}
|
volume 필드와 pitch 필드의 타입은 RangedFloat이다. Property Drawer로 RangedFloat 필드를 슬라이더 형태로 바꾸자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
using UnityEngine;
using UnityEditor;
[CustomPropertyDrawer(typeof(RangedFloat), true)]
public class RangedFloatDrawer : PropertyDrawer {
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
// | [label] [Inputbox] | 프로퍼티 행 추가
label = EditorGUI.BeginProperty(position, label, property);
// 시작부에 label 출력. 입력부의 position 반환.
position = EditorGUI.PrefixLabel(position, label);
SerializedProperty minProp = property.FindPropertyRelative("minValue");
SerializedProperty maxProp = property.FindPropertyRelative("maxValue");
float minValue = minProp.floatValue;
float maxValue = maxProp.floatValue;
// 어트리뷰트가 부여되지 않으면 이 값을 슬라이더 범위의 디폴트로 사용한다.
float rangeMin = 0;
float rangeMax = 1;
// fieldInfo는 PropertyDrawer의 멤버변수(필드)다. 리플렉션을 사용할 수 있다.
// 부여된 어트리뷰트의 생성자를 호출하고 개체를 반환한다.
var ranges = (MinMaxRangeAttribute[])fieldInfo.GetCustomAttributes(typeof (MinMaxRangeAttribute), true);
// 생성된 어트리뷰트의 개체가 있으면
if (ranges.Length > 0)
{
// '[MinMaxRange(0,2), MinMaxRange(1,3)]'와 같이 2개 이상 선언하지 않으므로
// 첫 번째 어트리뷰트만 참조
rangeMin = ranges[0].Min;
rangeMax = ranges[0].Max;
}
// 슬라이더 좌우 경계선에 위치할 라벨의 폭
const float rangeBoundsLabelWidth = 40f;
var rangeBoundsLabel1Rect = new Rect(position);
rangeBoundsLabel1Rect.width = rangeBoundsLabelWidth;
// 슬라이더 경계에 최소값을 f2(소수점 2자리) 형태로 출력
GUI.Label(rangeBoundsLabel1Rect, new GUIContent(minValue.ToString("F2")));
position.xMin += rangeBoundsLabelWidth;
var rangeBoundsLabel2Rect = new Rect(position);
rangeBoundsLabel2Rect.xMin = rangeBoundsLabel2Rect.xMax - rangeBoundsLabelWidth;
// 슬라이더 경계에 최대값을 f2(소수점 2자리) 형태로 출력
GUI.Label(rangeBoundsLabel2Rect, new GUIContent(maxValue.ToString("F2")));
position.xMax -= rangeBoundsLabelWidth;
EditorGUI.BeginChangeCheck();
// 슬라이더의 값 ref 변수로 반환
EditorGUI.MinMaxSlider(position, ref minValue, ref maxValue, rangeMin, rangeMax);
if (EditorGUI.EndChangeCheck())
{
// 프로퍼티에 값 반영
minProp.floatValue = minValue;
maxProp.floatValue = maxValue;
}
EditorGUI.EndProperty();
}
}
|
RangedFloat의 프로퍼티드로워다. RangedFloat필드를 슬라이더 형태로 바꿔서 Inspector뷰에 보여진다.
new position은 EditorGUI.PrefixLabe(position, label)가 호출되면 반환하는 것으로 Label이 쓰여지고 남은 프로퍼티 공간의 시작부를 가리킨다.
(Property Drawers in Unity --- Tutorial: short and quick - 125 seconds)
youtu.be/hnkZvGoNxh8
커스텀 에디터
인스펙터뷰에서 Preview 버튼을 누르면 구성된 클립을 재생할 수 있다. 커스텀 에디터를 사용해 "Preview"버튼을 생성해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(AudioEvent), true)]
public class AudioEventEditor : Editor
{
[SerializeField] private AudioSource _previewer;
public void OnEnable()
{
// Scene에 AudioSource 컴포넌트의 게임 오브젝트를 생성한다.
// HideFlags.HidAndDontSave "씬에서 숨겨서 편집 불가능. 생성과 제거를 명시적으로 해야함. Resources.UnloadUnusedAssets()로 언로드 되지 않음."
_previewer = EditorUtility.CreateGameObjectWithHideFlags("Audio preview", HideFlags.HideAndDontSave, typeof(AudioSource)).GetComponent<AudioSource>();
}
public void OnDisable()
{
DestroyImmediate(_previewer.gameObject);
}
public override void OnInspectorGUI()
{
// built-in 인스펙터 뷰를 그린다. 각 프로퍼티의 (커스텀 포함) PropertyDrawer.OnGUI()호출.
DrawDefaultInspector();
// Preview 버튼 추가.
EditorGUI.BeginDisabledGroup(serializedObject.isEditingMultipleObjects);
if (GUILayout.Button("Preview"))
{
((AudioEvent) target).Play(_previewer);
}
EditorGUI.EndDisabledGroup();
}
}
|
모든 AudioEvent들은 Preview할 수 있다. Editor의 타겟으로 AudioEvent를 명시하자.
'Unity > 스크립터블오브젝트' 카테고리의 다른 글
Scriptable Object - 응용: Dual Serialization, 세이브 & 로드 (8) (1) | 2020.06.03 |
---|---|
Scriptable Object - 응용: Destructible, Brain(AI) (7) (0) | 2020.06.02 |
Scriptable Object - 응용: Enum 대체, 오해와 진실, 프로퍼티 드로워 (5) (0) | 2020.06.01 |
Scriptable Object - 응용: 이벤트, 데이터셋 (4) (1) | 2020.06.01 |
Scriptable Object - 개념: 기본 아이디어, 기존 싱글톤의 장단점 (3) (0) | 2020.05.29 |
댓글