주녘공부일지

[해피스케치] 에디터 인스펙터 툴 (카메라, 스테이지) 본문

GameDevelopment/[Unity] Project

[해피스케치] 에디터 인스펙터 툴 (카메라, 스테이지)

주녘 2024. 12. 4. 16:58

플레이 영상

해피스케치의 요청에 따라 모션 감지를 이용한 AR게임 프로토타입 모델

- 여러 가지 미니게임 스테이지를 구현하여 2명의 사용자가 대전을 하는 AR 멀티 게임

- PC버전으로 선 구현 후 채택 시 AR 멀티 게임으로 확장

 

에디터 인스펙터 툴

- 기획자는 프로그래머를 거치지 않고, EditScene에서 각 스테이지를 플레이해보며 카메라 뷰를 변경하여 저장하거나 각 스테이지에서 사용되는 객체에 대한 엑셀 데이터 값을 변경하는 것으로 인게임에 대한 값 조정 및 테스트를 할 수 있게 함

1. 스테이지 에디터

- 인게임에서 사용되는 Stage를 소환하여 테스트해볼 수 있음

- 기획자가 직접 특정 스테이지를 플레이해보며 엑셀 데이터를 변경하여 제이슨으로 파싱하고 인게임에 적용된 데이터로 플레이해볼 수 있음

 ex) 몬스터 이동속도, 스턴 시간 등에 대한 데이터 등

 

 

 

 

 

 

#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
using static Define;

public class StageEditor : InitBase
{
    [SerializeField, ReadOnly] CameraEditor cameraEditor;
    [SerializeField, ReadOnly] BaseStage currStage = null;

    [SerializeField, ReadOnly] Player player = null;
    [SerializeField, ReadOnly] Transform playerStartPoint;

    [Header("[ 스테이지 세팅 영역 ]")]
    [Space(5f)]
    public EStageType StageType = EStageType.None;

    private void Start()
    {
        PlayStage();
    }

    public override bool Init()
    {
        if (base.Init() == false)
            return false;

        cameraEditor = FindObjectOfType<CameraEditor>();
        currStage = FindObjectOfType<BaseStage>();
        StageType = currStage.StageType;

        if (currStage == null)
        {
            Debug.LogError("스테이지를 소환하고 테스트해주세요.");
            UnityEditor.EditorApplication.isPlaying = false;
        }

        return true;
    }

    public void SpawnStage()
    {
        currStage = FindObjectOfType<BaseStage>();
        if (currStage != null)
        {
            GameObject.DestroyImmediate(currStage.gameObject);
        }

        if (StageType == EStageType.None)
        {
            Debug.LogWarning("소환할 스테이지 타입을 설정해주세요.");
            return;
        }

        string loadPath = $"{Application.dataPath}/Resources/Prefabs/{PrefabPath.STAGE_PATH}/{StageType}.prefab";
        GameObject go = PrefabUtility.LoadPrefabContents(loadPath);
        Util.Editor_InstantiateObject(go);
    }

    public void PlayStage()
    {
        Managers.Game.SetStageId((int)StageType);
        Managers.Game.StartStage();

        LightingController.SetStageLighting(StageType);
        playerStartPoint = Util.FindChild<Transform>(currStage.gameObject, "PlayerStartPoint", true);

        player = Managers.Resource.Instantiate($"{PrefabPath.OBJECT_PLAYER_PATH}/LeftPlayer").GetComponent<Player>();
        player.transform.position = playerStartPoint.position;
        player.transform.position += Vector3.up * player.GetColliderHeight();
        player.SetInfo((int)StageType);

        cameraEditor.SetTarget(player);
        
        switch(currStage)
        {
            case MultiStage multiStage:
                multiStage.SetInfo(player);
                multiStage.ConnectEvents(null);
                break;
            case SingleStage singleStage:
                singleStage.SetInfo(player, player);
                singleStage.ConnectEvents(null);
                break;
        }

        currStage.StartStage();
    }
}
#endif
using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(StageEditor))]
public class EditStage : Editor
{
    bool isSpawnStageUnlocked = false;

    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();

        StageEditor stageEditor = (StageEditor)target;

        GUILayout.Space(10);
        GUILayout.Label("[ 스테이지 에디터 ]", EditorStyles.boldLabel);

        GUILayout.Space(10);
        isSpawnStageUnlocked = EditorGUILayout.Toggle("스테이지 소환 잠금 해제", isSpawnStageUnlocked);
        GUILayout.Space(5);
        if (GUILayout.Button("스테이지 소환") && isSpawnStageUnlocked)
        {
            isSpawnStageUnlocked = false;
            stageEditor.SpawnStage();
            Debug.Log("스테이지 소환 완료");
        }

        GUILayout.Space(20);
    }
}

2. 카메라 에디터

- 인게임에서 사용되는 TeamCamera 클래스를 상속받아 CameraEditor를 구현

- 기획자가 직접 특정 스테이지의 카메라 세팅 값을 설정하여 런타임에 Save&Load 할 수 있음
- 스테이지가 로드되면 저장되어 있던 데이터를 스테이지 시작 시 로드하여 세팅하도록 설계

 ex) 몬스터 이동속도, 스턴 시간 등에 대한 데이터 등

 

 

 

 

 

 

#if UNITY_EDITOR
using System;
using System.IO;
using UnityEngine;

public class CameraEditor : TeamCamera
{
    [SerializeField, ReadOnly] StageEditor stageEditor;

    [Header ("[ 카메라 세팅 영역 ]")]
    [Space(5f)] [SerializeField, ReadOnly]
    EStageType stageType = EStageType.None;
    
    [Space(5f)] [SerializeField]
    BaseObject testTarget;

    [Space(10f)] 
    [Range(0f, 180f)]   [SerializeField] float fieldOfView = 60f;
    [Range(5f, 20f)]    [SerializeField] float targetDistance = 5f;
    [Range(-10f, 30f)]  [SerializeField] float cameraHeight = 5f;
    [Range(-10f, 40f)]  [SerializeField] float lookAtHeight = 0f;
    [Range(0f, 20f)]    [SerializeField] float nearClipping = 0.3f;
    [Range(20f, 1000f)] [SerializeField] float farClipping = 1000f;


    private void Update()
    {
        if(testTarget != null)
            SetTarget(testTarget);

        UpdateCameraInfo();
    }

    private void Start()
    {
        stageType = stageEditor.StageType;
    }

    public override bool Init()
    {
        if (base.Init() == false)
            return false;

        stageType = EStageType.None;
        cameraInfoData = new CameraInfoData(fieldOfView, targetDistance, cameraHeight, lookAtHeight, nearClipping, farClipping);

        stageEditor = FindObjectOfType<StageEditor>();

        return true;
    }

    public override void SetTarget(BaseObject target)
    {
        base.SetTarget(target);

        LoadCameraInfo();
    }

    public void InitCameraInfo()
    {
        fieldOfView = 60f;
        targetDistance = 5f;
        cameraHeight = 5f;
        lookAtHeight = 0f;
        nearClipping = 0.3f;
        farClipping = 1000f;
    }

    private void UpdateCameraInfo()
    {
        cameraInfoData.fieldOfView = fieldOfView;
        cameraInfoData.targetDistance = targetDistance;
        cameraInfoData.cameraHeight = cameraHeight;
        cameraInfoData.lookAtHeight = lookAtHeight;
        cameraInfoData.nearClipping = nearClipping;
        cameraInfoData.farClipping = farClipping;

        cam.fieldOfView = cameraInfoData.fieldOfView;
        cam.nearClipPlane = cameraInfoData.nearClipping;
        cam.farClipPlane = cameraInfoData.farClipping;
    }

    public bool LoadCameraInfo()
    {
        if(stageType == EStageType.None)
        {
            Debug.LogWarning("스테이지 타입을 설정하지 않았습니다.");
            return false;
        }

        bool isLoad = base.LoadCameraDataInfo(stageType);

        if(isLoad)
        {
            fieldOfView = cameraInfoData.fieldOfView;
            targetDistance = cameraInfoData.targetDistance;
            cameraHeight = cameraInfoData.cameraHeight;
            lookAtHeight = cameraInfoData.lookAtHeight;
            nearClipping = cameraInfoData.nearClipping;
            farClipping = cameraInfoData.farClipping;
        }

        return isLoad;
    }
    
    public bool SaveCameraInfo()
    {
        if (stageType == EStageType.None)
        {
            Debug.LogWarning("스테이지 타입을 설정하지 않았습니다.");
            return false;
        }

        string savePath = Application.dataPath + DataPath.STAGE_JSONDATA_PATH + $"/CameraData{(int)stageType}";
        Util.Editor_FileDelete(savePath);
        
        string jsonData = JsonUtility.ToJson(cameraInfoData);
        File.WriteAllText(savePath + ".json", jsonData);

        return true;
    }
}
#endif
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(CameraEditor))]
public class EditCamera : Editor
{
    bool isInitUnlocked = false;
    bool isLoadUnlocked = false;
    bool isSaveUnocked = false;

    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();

        CameraEditor cameraEditor = (CameraEditor)target;

        GUILayout.Space(10);
        GUILayout.Label("[ 카메라 에디터 ]", EditorStyles.boldLabel);

        // 초기 값 세팅
        GUILayout.Space(10);
        isInitUnlocked = EditorGUILayout.Toggle("초기 세팅 잠금 해제 ", isInitUnlocked);
        GUILayout.Space(5);
        if (GUILayout.Button("초기 값으로 세팅") && isInitUnlocked)
        {
            isInitUnlocked = false;
            cameraEditor.InitCameraInfo();
            Debug.Log("초기 값으로 세팅 완료");
        }

        // 데이터 불러오기
        GUILayout.Space(5);
        isLoadUnlocked = EditorGUILayout.Toggle("불러오기 잠금 해제 ", isLoadUnlocked);
        GUILayout.Space(5);
        if (GUILayout.Button("데이터 불러오기") && isLoadUnlocked)
        {
            isLoadUnlocked = false;
            if(cameraEditor.LoadCameraInfo())
                Debug.Log("스테이지 데이터 불러오기 완료");
        }

        // 데이터 저장
        GUILayout.Space(5);
        isSaveUnocked = EditorGUILayout.Toggle("저장하기 잠금 해제 ", isSaveUnocked);
        GUILayout.Space(5);
        if (GUILayout.Button("데이터 저장하기") && isSaveUnocked)
        {
            isSaveUnocked = false;
            if (!IsPlayEditor())
            {
                Debug.LogError("오류! : 세이브는 에디터 실행 상태에서 해야합니다.");
                return;
            }

            if (cameraEditor.SaveCameraInfo())
                Debug.Log("스테이지 데이터 저장하기 완료");
        }

        GUILayout.Space(20);
    }

    private bool IsPlayEditor() => Application.isEditor && Application.isPlaying;
}