유니티 숙련 주차 팀 프로젝트 하는 중
요즘 좀 뜸했는데... 이유가 다 있다... 일단 정말정말 쓸 말이 없었던 날들이 연속이었다. 이번 팀 프로젝트에서 맡은 부분이 맵 제작이었어서 그냥 오브젝트 배치만 하는 날이 있어서 공부할게 딱히 없었다. 그런데 왜 오늘은 글을 쓰느냐? 이제 맵 배치가 대략 끝나서 스크립트 작업에 들어갈 수 있게 되었기 때문이다. 두 가지를 적어볼까 한다.
Obstacle 레이어 주기이번게임에서 그림자 복도를 래퍼런스로 삼았기 때문에 만들어진 기능이다. 그복에는 귀신이 근처에 있으면 촛불이 깜박깜박 거리는 기능이 있다. 이 불빛으로 근처에 귀신이 있는지 판단이 가능해서 미리 조심할 수도 있고, 시각적으로 긴장감을 유발할 수 있다. 그걸 따라하기 위해 만든 스크립트!
내가 생각했던 감지 방식은 촛대가 두 개의 감지 범위를 가지는 것이었다.
[Header("불 깜박임")]
public float flickerDistance = 10f;
[Header("불 꺼짐")]
public float offDistance = 2f;
[Header("깜박임 주기")]
public float flickerIntervalMin = 0.2f;
public float flickerIntervalMax = 1f;
private Coroutine flickerCoroutine;
private float baseIntensity= 5f;
private readonly List<Transform> monsters = new List<Transform>();//트리거로 감지된 몬스터 리스트
처음에는 불이 일정하게 깜박이게 했었는데 그러면 좀 부자연스러운것 같아서 깜박임 주기를 0.2~1에서 랜덤 주기로 깜박이게 수정하였다. 애니메이션으로 만들까 했는데 이 얘기를 들은 팀장님이 랜덤함수를 쓰라고 조언을 주셔서 적극 반영했다. 몬스터를 리스트로 받는 이유는 여러마리가 있을 수 있어서!
void Update()
{
LightState();
}
void LightState()
{
// 몬스터가 없다면
if (monsters.Count == 0)
{
StopFlicker();//깜박임 중지
candleLight.enabled = true;
flame.SetActive(true);
candleLight.intensity = baseIntensity;
return;
}
// 가장 가까운 몬스터와 거리계산
float nearest = float.MaxValue;
Vector3 pos = transform.position;
for (int i = monsters.Count - 1; i >= 0; i--)
{
if (monsters[i] == null)
{
monsters.RemoveAt(i);
continue;
}
float dist = Vector3.Distance(pos, monsters[i].position);
if (dist < nearest) nearest = dist;
}
//불 꺼짐 거리와같거나 작다면
if (nearest <= offDistance)
{
// 깜박임 멈추고 불 꺼짐
StopFlicker();
candleLight.enabled = false;
flame.SetActive(false);
}
else if (nearest <= flickerDistance)
{
// 깜박거려라
StartFlicker();
}
else
{
// 정상 밝기
StopFlicker();
candleLight.enabled = true;
flame.SetActive(true);
candleLight.intensity = baseIntensity;
}
}
촛대에 콜라이더를 달아서 monster태그가 있는 괴물이 첫 번째 감지 범위에 들어오면 깜박거리고, 두 번째(더 가까운)감지 범위에 들어오면 촛불이 꺼지는 방식을 생각했다. 그런데 콜라이더로 감지할건데 두 개의 범위를 감지하게 하려면 콜라이더를 두 개 가져야 하는지? 근데 그렇게 되면 각각 어떻게 감지를 시켜야 할지 고민이 되어서 튜터님께 상담하러 갔었다. 튜터님은 콜라이더는 하나만 두고 촛대 오브젝트와 괴물간의 거리 계산으로 범위를 정하는게 어떻냐는 말을 하셨고 적용해보았더니 잘 동작하였다!
이번에 맵 제작을 담당했는데 그림자 복도의 맵을 그대로 따라 만들기로 했다. 그림자 복도는 맵이 매우 큰 편인데 우리 게임은 그 정도 규모는 되지 않으므로 좀 작았는데 실내인 만큼 천장을 덮어야 했다.
그런데 플레이 할땐 천장이 있고, 작업할 땐 없어야 하니(씬에서 작업하기에 천장이 막고 있으면 매우 불편) 원래는 일일히 비활성화를 해줬어야 했는데 아... 생각보다 너무너무 불편했다.
나만 작업하는 것이 아니고 이 씬을 복사해서 팀원들도 가져다가 쓸텐데 이렇게 불편해서야 작업하기 너무 힘들 것 같았다. 그래서 저번 실시간 세션에서 배운 커스텀 에디터기능을 사용해서 천장을 인스펙터 창에 있는 버튼으로 한번에 활성화 / 비활성화 하는 기능을 만들었다.
public class MultiGroupSwitcher : MonoBehaviour
{
[Serializable]
public class ToggleGroup
{
[Header("그룹 이름")]
public string groupName;
[Header("오브젝트 넣기")]
public GameObject[] objects;
}
[Header("개별 그룹")]
public ToggleGroup[] groups;
}
이걸 빈 오브젝트에 스크립트로 넣어주고 오브젝트를 드래그 앤 드롭으로 넣어주면 된다.
[CustomEditor(typeof(MultiGroupSwitcher))]
public class MapSwitcherEditor : Editor
{
public override void OnInspectorGUI()
{
// 기본 인스펙터 렌더링
DrawDefaultInspector();
// 버튼 영역
EditorGUILayout.Space();
var manager = (MultiGroupSwitcher)target;
// 각 그룹 버튼 그리기
EditorGUILayout.LabelField("그룹별 버튼", EditorStyles.boldLabel);
for (int i = 0; i < manager.groups.Length; i++)
{
var group = manager.groups[i];
if (string.IsNullOrEmpty(group.groupName))
group.groupName = "Group " + (i + 1);
GUILayout.BeginHorizontal();
EditorGUILayout.LabelField(group.groupName, GUILayout.Width(60));
if (GUILayout.Button("끄기 켜기", GUILayout.Width(100)))
{
// 토글 로직
foreach (var go in group.objects)
{
if (go != null)
go.SetActive(!go.activeSelf);
}
EditorUtility.SetDirty(manager);
}
GUILayout.EndHorizontal();
}
}
}
커스텀 에디터를 생성하려면 Editor를 상속받아 만들어야 한다. 스크립트를 Editor 폴더에 두면 Unity가 런타임이 아닌 에디터 전용 코드로 인식해 빌드에 포함되지 않고 인스펙터에만 적용한다.
DrawDefaultInspector() 로 MultiGroupSwitcher에 있는 groups 배열 같은 기본 프로퍼티를 그대로 렌더링한다.

이런 느낌!
for (int i = 0; i < manager.groups.Length; i++)
{
var group = manager.groups[i];
if (string.IsNullOrEmpty(group.groupName))
group.groupName = "Group " + (i + 1);
이 부분은 각 ToggleGroup에 지정된 이름을 표시해준다. 만약 빈 문자열이라면 기본 이름을 자동으로 채워준다.
foreach (var go in group.objects)
if (go != null)
go.SetActive(!go.activeSelf);
EditorUtility.SetDirty(manager);
버튼을 클릭하면 해당 그룹에 등록된 GameObject[]를 순회하며 go.activeSelf를 뒤집어서 SetActive(!..)로 토글한다. 그리고 EditorUtility.SetDirty(manager);를 호출해서 인스펙터가 변경을 감지하고 씬에 즉시 반영되도록 한다.
처음에는 천장만 버튼을 만들었는데 문도 있으면 좋을 것 같아서 문도 추가했다. 문을 비활성화 할 일은 많지 않았지만 프로젝트 후반에 네비게이션을 사용할 때 문이 있는 상태면 방으로 가는 틈이 bake되지 않는 문제가 있어서 문을 없애고 다시 bake해야 했던 일이 있었는데 이때 문을 한번에 활성화 / 비활성화 버튼이 요긴하게 쓰였다. 팀원들이 잘 써주는 모습을 보니 만들기 정말 잘했다는 생각이 들었다. 일머리가 좋다는 얘기까지 들어서 기분이 해피해피!!
역시 모르겠거나 헷갈리는 부분이 있으면 자주 물어봐야 한다. 고민하던 부분을 튜터님께 가져가니 내가 모르던 좋은 해결법을 알려주셨고, 또 팀원들한테 말하니 내가 생각했던 것보다 더 효율적인 방법을 알려줬다. 소통이 최고야!
내일은 진짜 마지막으로 합쳐봐야 하는 날이다. 물론 수요일도 있지만 그 날엔 시연 영상도 찍어야 하고 발표 자료도 만들어야 한다. 구현이나 하고 있을 시간이 별로 없다... 그런데 지금 진도가 조금 느려서 내일 진짜 완료해야한다. 내일 으으 화테엥