오늘의 학습 키워드

ScriptableObject

 

공부한 내용

스크립터블 오브젝트로 아이템 만들고 관리하기

아이템의 데이터를 인벤토리 UI에 넘겨 받고 인벤토리 UI에서 다른 UI로 아이템 데이터를 넘겨주기 위해 스크립터블 오브젝트를 사용하게 되었다.

인벤토리의 아이템 UI를 클릭하면 그 해당 아이템의 정보가 팝업에 넘겨지게 되며 UI가 갱신된다.

장착하면 아이템 UI에 E라는 표시가 뜨게 되고 장착을 해제하면 E가 사라진다.

 

아이템 데이터 전달에 대한 고민

고민 : 인벤토리 아이템 데이터를 팝업UI에게 넘겨주는 과정에서 아이템의 정보를 어떻게 넘겨야 할지 생각해보았다.

시도한 것 : 팝업 UI에서 현재 아이템UI를 가지고 있는 방법이다.

문제점 : 팝업 UI가 현재 아이템 정보를 가지게 되면 인벤토리와의 중복 데이터가 생기게 될 수도 있고 구조상 크게 관련 없는 객체가 다른 객체를 참조하게 된다.(굳이? 느낌)

public class UsePanel : MonoBehaviour
{
    private ItemUI _currentItemUI;
}

시도한 것 2 : 인벤토리에서 콜백으로 아이템UI에게 데이터만 넘기고 명령은 인벤토리에서 이루어지는 방법 - 구조상으로도 관리자가 시키는 방식

private void InitItems()
{
    foreach (var item in items)
    {
        ItemUI itemUI = Instantiate(itemUIPrefab, itemFrameArr[_currentIdx].transform).GetComponent<ItemUI>();
        itemUI.SetData(item.itemData);
        itemUI.OnItemClicked += ActiveUsePopup;
        _currentIdx++;
    }

    usePanel.OnUsed += OnUsedItem;
}

private void ActiveUsePopup(bool isActive, ItemUI itemUI)
{
    ItemData tempData = itemUI.Data;
    ItemData newData = InitNewData(tempData);

    itemUI.SetData(newData);
    _currentItemUI = itemUI;
    usePanel.SetCurrentItemData(itemUI.Data);
    usePanel.gameObject.SetActive(isActive);
}

 

 

오늘의 회고

  오늘은 개인 과제에서 인벤토리 관련 부분을 집중적으로 진행했다. 데이터는 어떻게 넘겨야 할지 구조에 어떻게 짜야될지에 대해서도 고민을 많이한 날이었다. 튜터님의 선발대 강의도 있었는데 UI 관련해서 팝업 시스템을 알려주셨다. 팀 프로젝트 때는 팝업 시스템을 구현해봤으면 좋겠다는 생각이 들었다. 또한 다음 프로젝트 때는 무한 스크롤뷰라는 기능을 한 번 구현해봐야겠다는 생각도 들었다.

 내일은 과제 제출날이다. 열심히 한 만큼 잘 정리해서 제출하도록 하자. 내일도 파이팅!

오늘의 학습 키워드

Raw Image, ScrollView

 

공부한 내용

캐릭터 UI에 띄우기

캐릭터를 UI에 띄우려고 하는데 저번에 업 스케일링해서 했던 것처럼 Canvas에 RawImage를 적용하는 방식으로 시도해봤다.

문제점 : UI 카메라에서 적용했던 것처럼 Flags를 Clear Depth로 하면 잔상이 남는다는 것이었다.

해결 : Raw Image를 넣는 카메라의 Flags를 Solid Color로 바꾸고 나니 UI 상에서 잔상이 사라졌다.

캐릭터 카메라를 띄우는 소스코드는 다음과 같다.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class CharacterCamera : MonoBehaviour
{
    private Camera _characterCamera;
    [SerializeField] private Canvas characterCanvas;

    private void Awake()
    {
        _characterCamera = GetComponent<Camera>();    
    }

    private void Start()
    {
        CreateRT();
    }

    private void CreateRT()
    {
        RenderTexture rt = new RenderTexture(Screen.width, Screen.height, 24, RenderTextureFormat.Default);

        rt.Create();
        _characterCamera.targetTexture = rt;

        RawImage raw = Instantiate(characterCanvas.gameObject).GetComponentInChildren<RawImage>();
        raw.texture = _characterCamera.targetTexture;
    }
}

관련 일지 :

https://jcdevelop98.tistory.com/331

 

내일배움캠프 23일차 TIL - UpScale Sampling

오늘의 학습 키워드 UpScale Sampling 공부한 내용 UpScale Sampling 업 스케일링 샘플링 : UI는 원래 해상도로 하고 3D 씬만 낮은 해상도로 렌더링 하는 방법 - 해상도를 줄이면 픽셀이 도드라져 게임 유저

jcdevelop98.tistory.com

 

 

스크롤 뷰 사이즈 자동화

문제점 : 스크롤 뷰에서 콘텐츠 사이즈를 수동으로 정해줄 때 개수가 늘어나거나 줄어든다면 사이즈도 다시 수동으로 바꿔줘야 한다는 것이 불편했다.

내가 구현한 방법 : 먼저 배열에 자식들을 담고 아이템들을 정렬하는 그룹인 Grid Layout Group의 요소들(간격)을 가져와 아이템 사이즈와 더했다. 그 이후 아이템의 열 개수 만큼(세로 스크롤) 사이즈를 늘려주도록 했다.

이후에 게임을 시작하면 사이즈에 맞게 Height가 조절된 것을 볼 수 있다.

private void InitItemArr()
{
    int i = 0;

    foreach (RectTransform trans in _inventoryRectTramsform)
    {
        itemArr[i] = trans.gameObject;
        i++;
    }
}

private void InitContentSize()
{
    _totalItemSize = itemSpace + itemSize;
    float initYSize = (itemArr.Length / initRowSize - initColumnSize) * _totalItemSize;
    Vector2 sizeDelta = _inventoryRectTramsform.sizeDelta;
    sizeDelta.y = initYSize;
    _inventoryRectTramsform.sizeDelta = sizeDelta;
}

 

 

오늘의 회고

 오늘부터 TextRPG UI에 대한 개인과제를 진행했다. MainUI와 StatusUI까지 구현을 끝내고 Inventory 구현을 하다가 말았는데 아마 오늘 작업보다 내일 작업이 더 많을 듯 싶다. 내 욕심으로썬 스크립터블 오브젝트까지 구현해서 데이터랑 연동하는 것까지 가능하다면 좋을 것 같은데 아마 과제 제출이 이미 끝난 상태에서 적용하지 않을까 싶다. 그래도 될 수 있는한 끝까지 열심히 해볼 생각이다.

 오늘 할 게 많아서 무아지경 상태에서 한 것 같은데 내일도 이렇게만 집중해서 열심히 해보자. 파이팅!

오늘의 학습 키워드

NavMesh.SamplePosition

 

공부한 내용

NavMesh.SamplePosition

NavMesh.SamplePosition(Vector3 sourcePosition, out NavMeshHit hit, float maxDistance, int areaMask)

첫 번째 인자는 기준 위치,

두 번째 인자는 결과 위치의 정보를 담는 NavMeshHit,

세 번째 인자는 검출할 최대 거리

네 번째 인자는 NavMesh 영역의 마스크이다.

이 메서드가 하는 일은 기준 위치에서 maxDistance만큼 areaMask만 검출해서 hit으로 정보를 받아오고 hit이 존재한다면 true를 반환한다.

아래의 코드에서 왜 30번이나 while문을 돌아야 하는지 이해가 되지 않았었다.

이후에 궁금해서 공식 문서를 찾아보았고 이유를 찾게 되었다.

충돌 영역을 체크하는 것은 보편적으로 비용이 많이 들기 때문작은 영역을 여러 번 체크하는 것이 좋다고 나와있었다.

하지만 30번을 왜 돌아야 하는지는 아직도 의문이 있었는데 이해가 되지 않아서 튜터님께 질문을 드렸다.

그 이유는 계속 찾지 못하는 경우라면 while문을 빠져 나오지 못해 무한루프 상태에 돌입돼서 임의의 값을 지정해 주는 것이라고 하였다.

int i = 0;
while (GetDestinationAngle(hit.position) > 90f || playerdistance < safeDistance)
{
    NavMesh.SamplePosition(
    transform.position + UnityEngine.Random.onUnitSphere * safeDistance,
    out hit,
    maxWanderDistance,
    NavMesh.AllAreas);
    i++;
    if (i == 30)
        break;
}

return hit.position;

 

 

오늘의 회고

 오늘까지 개인 과제 수행 전에 공부를 마치고 내일부터 개인 과제를 진행할 생각이다. 내가 생각한 것보다 약간 늦어졌지만 더 시간을 보태서 개인 과제를 마무리 할 수 있도록 노력해야겠다. UI 쪽 집중 구현과 C#으로 개발하던 TextRPG를 유니티로 만들어 보는 과제인데 TextRPG가 더 재밌을 것 같아서 아마 그쪽으로 진행하지 않을까 싶다.

 오늘은 코드카타를 진행하면서 한 문제에서 오래 걸려서 많이 풀지 못했는데 내일은 더 많이 풀어봐야겠다. 내일도 파이팅!

 

 

참고 : 

https://docs.unity3d.com/kr/530/ScriptReference/NavMesh.SamplePosition.html

 

NavMesh-SamplePosition - Unity 스크립팅 API

Finds the closest point on NavMesh within specified range.

docs.unity3d.com

 

오늘의 학습 키워드

낮과 밤 구현 해석, 1인칭 회전

 

공부한 내용

낮과 밤

다음과 같은 코드를 해석하려고 그림을 그려가면서 그리고 유니티에서 어떻게 동작하는가 보면서 이해해보았다.

Update에서 불리는 time이 더해지면서 1로 나누는 로직에 따라 라이팅의 회전이 변하는 코드이다.

time = (time + timeRate * Time.deltaTime) % 1.0f;
lightSource.transform.eulerAngles = (time - (lightSource == sun ? 0.25f : 0.75f)) * noon * 4.0f;

가장 중요한 부분해가 뜰 때를 이해하는 것이었다.

해가 뜰 때 기준으로 6시니 time은 0.25겠고 lightSource가 sun이니 0.25f - 0.25f == 0이니까 각도가 0으로 나오게 된다.

이것만 이해해도 다른 것들이 이해가 될 것이다.

여기서 이해가 가지 않는다면 낮 12시에 해의 각도가 90도(위에서 아래로 내리쬐는 각도)라는 것을 기억하면서 그림을 봐보자.

 

 

1인칭 카메라 회전

1인칭 카메라 회전을 구현할 때 x축과 y축의 순서 또는 짐벌락 현상 때문에 고생을 한 적이 있을 것이다.

여기 예제에서는 y축 회전을 transform.eulerAngles로 부모에서 처리하고 x축 회전을 localEulerAngles로 자식에서 처리하는 것으로 각 회전이 독립적으로 수행될 수 있도록 하였다.

void CameraLook()
{
    camCurXRot += mouseDelta.y * lookSensitivity;
    camCurXRot = Mathf.Clamp(camCurXRot, minXLook, maxXLook);
    cameraContainer.localEulerAngles = new Vector3(-camCurXRot, 0, 0);

    transform.eulerAngles += new Vector3(0, mouseDelta.x * lookSensitivity, 0);
}

부모와 자식의 x,y 회전 값을 반대로 수행한다면 안 되는 것을 볼 수 있었다.

찾아보니 이것은 유니티의 회전 순서 때문인 것으로 파악이 된다.

z -> x -> y 순으로 진행해야 제대로 회전이 될 수 있다.

그래서 여기서 알 수 있는 사실은 부모 자식간의 회전은 자식이 먼저 실행 된 뒤 부모가 실행된다.

void CameraLook()
{
    camCurXRot += mouseDelta.y * lookSensitivity;
    camCurXRot = Mathf.Clamp(camCurXRot, minXLook, maxXLook);
    cameraContainer.localEulerAngles = new Vector3(0, mouseDelta.x * lookSensitivity, 0);
    
    transform.eulerAngles += new Vector3(-camCurXRot, 0, 0);
}

 

 

오늘의 회고

 오늘은 낮과 밤 시스템과 1인칭 회전에 대해서 배웠다. 낮과 밤 시스템은 처음에 이해하기 어려웠으나 전자 보드에서 쓱쓱 그려보고 대충 개념을 잡은 뒤 나중에 정리하면서 피그마로 그려봤는데 이해가 된 것 같아 다행이다. 또한 1인칭 회전은 왜 저렇게 수행하는지 이유를 몰랐었는데 이번에 정확히 알게 되어서 기분이 좋다.

 내일은 좀 더 공부를 진행한 뒤 개인 과제를 수행할 계획이다. 시간이 많지는 않아서 최대한 빨리 진행하려고 생각하고 있다. 내일도 열심히 해보자. 파이팅!

 

 

참고 : 

https://docs.unity3d.com/kr/2021.3/Manual/QuaternionAndEulerRotationsInUnity.html#:~:text=Unity%EB%8A%94%20Z%EC%B6%95%2C%20X,%EC%A2%8C%ED%91%9C%EA%B3%84%EA%B0%80%20%EB%B3%80%EA%B2%BD%EB%90%98%EC%A7%80%20%EC%95%8A%EC%8A%B5%EB%8B%88%EB%8B%A4.

 

Unity의 회전 및 방향 - Unity 매뉴얼

Unity에서는 오일러 각과 쿼터니언을 모두 사용하여 회전과 방향을 나타낼 수 있습니다. 표현은 둘 다 동일하지만 용도와 제한 사항은 서로 다릅니다.

docs.unity3d.com

 

오늘의 학습 키워드

Quaternion, LayerMask, 연산자 우선순위

 

공부한 내용

Quaternion과 벡터의 곱셈

쿼터니언과 벡터를 곱하면 다음과 같은 작업이 이루어진다.

1. 벡터를 쿼터니언으로 변환 : v = (x,y,z)인 벡터는 (0,x,y,z)의 쿼터니언으로 변환됨

2. 이렇게 변환된 쿼터니언을 원래의 쿼터니언 q와 그 켤레 쿼터니언 q'의 사이에 두고 곱셈을 수행(qvq)한다. (순서 중요 ! => 순서에 따라서 값이 달라짐)

3. 결과 쿼터니언의 (x,y,z)요소는 원래 벡터 v가 쿼터니언 q에 의해 회전한 후의 좌표를 나타냄

아래 예시는 벡터 v를 축으로 degree만큼 회전한 벡터 값이 나온다.

private static Vector2 RotateVector2(Vector2 v, float degree)
{
    // 벡터와 쿼터니언을 곱하면 벡터에서 쿼터니언으로 회전한 벡터 값이 나온다.
    return Quaternion.Euler(0, 0, degree) * v;
}

 

레이어마스크와 비트 연산과 쉬프트 연산

게임 오브젝트에는 레이어가 있는데 레이어 마스크레이어를 비트 마스크로 포맷한 정수이다.

예시로 레이어가 6이라면 레이어 마스크는 비트 연산을 통해 64가 된다.

레이어

근데 아래의 상황에서 한 가지 의문점이 생겼다. 

| 비트 연산이 << 쉬프트 연산보다 먼저 일어나면 문제가 생기는 것이 아닐까? 라는 의문이었다.

괄호를 넣더라도 안 넣더라도 결과가 같기 때문에 무슨 이유가 있을 것이라고 생각했다.

그래서 어떤 게 더 빠른지 찾아보게 되었다.

답은 연산자 우선순위라는 것이 있었고 어렴풋이 예전에 공부했던 기억이 났다.

쉬프트 연산자가  [|, &, ^] 비트 연산자보다는 빠르고 [~] 비트 연산자보다는 느리다고 한다.

levelCollisionLayer.value == (levelCollisionLayer | 1 << collision.gameObject.layer)

 

 

오늘의 회고

 오늘은 유니티 컨퍼런스를 듣는 시간이 있었다. 내가 주목해서 봤던 것은 ProBuilder나 ChatGPT를 이용해 플랫포머 기획부터 개발까지 하는 것이었다. ProBuilder는 프로토타입을 만들기에 좋은 에셋인 것 같았고 ChatGPT는 클라이언트 입장에서 기획쪽에서 도움을 받아 개발에 집중할 수 있을 것 같았다. 나중에 프로젝트를 만든다면 ChatGPT의 도움을 받는 것도 괜찮다고 생각한다.

 다음 주는 유니티 심화 개인 과제가 주어지는 주이다. 정말 배울 것이 많고 질문할 것도 많아서 엄청나게 성장할 수 있는 기회라고 생각한다. 다음 주 열심히 해보자. 파이팅!

 

 

참고 : 

http://www.tcpschool.com/codingmath/priority

 

코딩교육 티씨피스쿨

4차산업혁명, 코딩교육, 소프트웨어교육, 코딩기초, SW코딩, 기초코딩부터 자바 파이썬 등

tcpschool.com

https://docs.unity3d.com/kr/2022.2/Manual/layers-and-layermasks.html

 

레이어와 레이어마스크 - Unity 매뉴얼

모든 게임 오브젝트는 단일 레이어에 존재하지만 API가 영향을 미치는 레이어를 설정할 수 있는 Unity API는 레이어를 직접 사용하지 않습니다. 대신 레이어마스크를 사용합니다.

docs.unity3d.com

 

오늘의 학습 키워드

Static Batching, Dynamic Batching, Gpu Instancing

 

공부한 내용

Static Batching

Static Batching은 드로우 콜을 줄이기 위한 배칭의 한 기법으로 움직이지 않는 여러 오브젝트가 같은 Material을 공유할 때 메시를 통째로 메모리에 같이 올려 한 번에 그려주는 기능이다.

특징

- 정적인 오브젝트 대상 (인스펙터에서 static 체크)

- 여러 메시를 통째로 미리 메모리에 올리기 때문에 추가적인 메모리 필요

- 런타임 전에 상태 변경 명령을 수행

- 드로우콜 감소 -> CPU 병목 완화

*드로우 콜 : CPU가 GPU에게 상태 변경 명령부터 렌더링까지 명령하는 것

Static Batching 전
Static Batching 후

 

Dynamic Batching

Static Batching은 드로우 콜을 줄이기 위한 배칭의 한 기법으로 움직일 수 있는 동일한 메쉬를 가진 여러 오브젝트가 같은 Material을 공유할 때 런타임 중에 버텍스를 모아 한 번에 그려주는 기능이다.

특징

- Skinned Mesh에는 적용 불가

- 버텍스 수가 일정 수치보다 높으면 적용 불가

- 런타임 중에 버텍스 정보를 읽어오므로 오버헤드 증가

- 드로우 콜 감소 -> CPU 병목 완화

Dynamic Batching 전
Dynamic Batching 설정
Dynamic Batching 후

 

GPU Instancing

GPU Instancing은 동일한 메시의 복사본들을 만들어 별도의 메시를 생성하지 않고 GPU에서 원본 메시를 가져다가 여러 오브젝트를 한 번에 처리해서 렌더링한다.

특징

- GPU에서 인스턴싱 처리 -> 오버헤드나 메모리 이슈에서 자유로움 (버텍스 수로부터 자유로움)

- 동일한 모양의 오브젝트들이 많이 렌더링 되어야 할 때 유용한 기법

GPU Instancing 전
Inspector 설정
GPU Intancing 후

 

Mesh.CombineMeshes

Mesh.CombineMeshes는 동일한  Material을 공유하는 메시끼리 스크립트를 통해서 합쳐주는 메서드이다.

- 런타임 동안 파츠가 조합되어 오브젝트가 만들어져야 하는 경우라면 고려할만 하다.

Mesh Combine 전
Mesh Combine 후

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// 모델링 과정에서 하나의 오브젝트로 만들어져 생성하는 것이 좋겠지만
// 같은 Material을 공유하고 파츠별로 바꿔서 조립하며 버텍스 수가 많아 다이나믹 배칭이 어려운 상황에서는
// CombineMesh도 고려할만 하다.(배치와 SetPass 감소)

public class CombineMesh : MonoBehaviour
{
    void Start()
    {
        CombineMeshs();
    }

    private void CombineMeshs()
    {
        // 자식들의 메쉬 필터와 렌더러를 가져옴
        MeshFilter[] meshFilters = GetComponentsInChildren<MeshFilter>();
        MeshRenderer[] meshRenderers = GetComponentsInChildren<MeshRenderer>();

        // 컴바인인스턴스 배열을 메쉬 필터의 길이만큼 생성
        CombineInstance[] combineInstances = new CombineInstance[meshFilters.Length];
        if (CanMeshCombine(meshRenderers))
        {
            // 메쉬 필터, 렌더러 정보를 컴바인인스턴스에 저장하고 게임 오브젝트 다 끄기(자식)
            for (int i = 0; i < combineInstances.Length; i++)
            {
                combineInstances[i].mesh = meshFilters[i].sharedMesh;
                combineInstances[i].transform = meshFilters[i].transform.localToWorldMatrix; // 트랜스폼 로컬 좌표를 월드 좌표로 변환
                meshFilters[i].gameObject.SetActive(false);
            }

            // 새 메쉬 필터와 렌더러 컴포넌트 추가
            MeshFilter meshFilter = gameObject.AddComponent<MeshFilter>();
            MeshRenderer meshRenderer = gameObject.AddComponent<MeshRenderer>();

            // 메쉬 렌더러 Material 초기화
            meshRenderer.sharedMaterial = meshRenderers[0].sharedMaterial;

            // 새 메쉬 생성 후 메쉬 필터에 넣어놓고 메쉬를 합침
            Mesh newMesh = new Mesh();
            meshFilter.mesh = newMesh;

            //메쉬 합치기
            newMesh.CombineMeshes(combineInstances);

            // 이후 오브젝트 켜주기(부모)
            transform.gameObject.SetActive(true);
        }
    }

    // Material이 같다면
    private bool CanMeshCombine(MeshRenderer[] renderers)
    {
        Material material = renderers[0].sharedMaterial;
        for (int i = 1; i < renderers.Length; i++)
        {
            if (material != renderers[i].sharedMaterial)
            {
                return false;
            }
        }

        return true;
    }
}

 

 

오늘의 회고

  오늘은 팀 프로젝트 제출과 발표가 있었다. 나는 오늘 오전에 버그를 잡고 시연 영상을 찍는 작업을 했는데 시연 영상을 찍을 때마다 새로운 버그가 나와서 바로바로 고쳐서 겨우 제출할 수 있었다. 오후에는 개인 공부 시간을 가졌는데 최적화 관련해서 배칭과 드로우콜 부분을 공부하게 되었다. CPU 병목일 때 드로우콜을 낮추기 위해 static batching, Dynamic Batching, GPU Instancing을 고려해봐야겠다는 생각이 들었다.

 내일도 최적화 부분을 좀 더 공부하지 않을까 싶다. 내일도 파이팅!

오늘의 학습 키워드

CustomEditor

 

공부한 내용

CustomEditor

게임 플레이 중에 바로 변화를 보기 위해 CustomEditor를 이용하게 되었다.

왜 저번처럼 CustomWindow를 사용하지 않고 CustomEditor를 사용했나면 GameManager의 기능은 모든 씬 전체에 영향을 주는 것이 아니고 존재하지도 않기 때문에 이 스크립트가 존재할 때만 적용할 생각이기 때문이다. 

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

[CustomEditor(typeof(GameManager))]
public class GameHelperEditor : Editor
{
    GameManager gm;

    private void OnEnable()
    {
        gm = (GameManager)target;
    }

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

        EditorGUILayout.LabelField("Helper", new GUIStyle(EditorStyles.label)
        {
            alignment = TextAnchor.MiddleCenter,
        });

        if (GUILayout.Button("AddScore100"))
        {
            gm.AddScore(100);
        }
        if (GUILayout.Button("IncreaseLife"))
        {
            gm.IncreaseLife();
        }
        if (GUILayout.Button("DecreaseLife"))
        {
            gm.DecreaseLife();
        }
        if (GUILayout.Button("GameClear"))
        {
            gm.Clear();
        }
        if (GUILayout.Button("GameOver"))
        {
            gm.Over();
        }
    }
}

 

 

오늘의 회고

  오늘은 다른 팀원의 작업을 도와주면서 버그를 잡는 작업을 많이 했던 것 같다. 내일 제출이기 때문에 우리가 목표로 한 작업들까지는 버그가 없이 잘 동작해야 한다고 생각을 해서 이 부분에 집중을 했다. 그리고 CustomEditor를 사용하면서 버그가 검출이 됐는데 아 이래서 처음부터 테스트용 목적으로 에디터를 만들어 놓는구나 하는 생각이 들었다.

 내일은 이번에 진행한 프로젝트의 제출일이다. 내일은 readme 작성과 영상을 찍고 제출하는 것을 할 생각이다. 내일도 파이팅!

오늘의 학습 키워드

EditorWindow, CustomEditor

 

공부한 내용

CustomEditor

커스텀 에디터는 MonoBehaviour를 가진 스크립트를 타겟으로 하여 OnInspectorGUI 메서드를 사용하여 인스펙터에 커스텀으로 버튼, 레이블 등을 추가할 수 있는 기능이다.

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

[CustomEditor(typeof(GameHelper))]
public class GameEditor : Editor
{
    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();

        if (GUILayout.Button("게임 시작"))
        {
            Debug.Log("게임 시작");
        }
    }
}

 

EditorWindow

에디터 윈도우는 따로 윈도우 창(탭)을 만들어 오브젝트에 종속되지 않고도(타겟을 정할 순 있음) 에디터를 커스텀 할 수 있는 기능이다.

이번 경우엔 CustomEditor보다 EditorWindow를 사용했다.

처음에 CustomEditor로 구현하다가 타겟을 정해야되는 부분에서 고민을 좀 했는데

씬을 이동하는 것을 구현하는 점에서 굳이 파괴될 수 있는 씬에 있는 오브젝트를 대상으로 타겟을 설정하는 것이 별로라고 생각했다.

 

씬 이동에서의 문제점 1

여기서 한 가지 문제가 있었는데 에디터 상에서 씬을 이동할 때는 EditorSceneManager.OpenScene()을 이용해야 한다는 점이었다. 플레이 모드에서 씬을 이동할 때는 SceneManager.LoadScene()을 사용해야하기 때문에 Applicaition.IsPlaying으로 분기 처리를 해주었다.

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

[Serializable]
public class SceneWindow : EditorWindow
{
    private const string PATH = "Assets/Scenes/";

    private SceneType _sceneType;

    [MenuItem("Helper/SceneWindow")]
    static void Init()
    {
        EditorWindow window = GetWindow(typeof(SceneWindow));
        window.maximized = true;
        window.Show();
    }


    private void OnGUI()
    {
        _sceneType = (SceneType)EditorGUILayout.EnumPopup(_sceneType, new GUIStyle(EditorStyles.popup) 
        { 
            alignment = TextAnchor.MiddleCenter,
        });

        if (GUILayout.Button("이동"))
        {
            if (Application.isPlaying)
            {
                SceneManager.LoadScene($"{PATH}{_sceneType}.unity");
            }
            else
            {
                EditorSceneManager.OpenScene($"{PATH}{_sceneType}.unity");
            }

        }
        
        if (!Application.isPlaying && GUI.changed)
        {
            EditorUtility.SetDirty(this);
            EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
        }
    }
}

 

씬 이동에서의 문제점 2

갑자기 씬이 안 넘어가지는 버그가 생겼다.

디버그를 해봤더니 버튼까지는 잘 동작되는 것을 확인할 수 있었다.

그래서 뭐가 문제인지 봤더니 저번에 공부했던 Time.deltaTimeTime.unscaledDeltaTime과 관련한 문제였다.

GameManager에서 게임이 끝나면 TimeScale을 0으로 만드는데 여기서 버그가 발생한 것이었다.

Time.deltaTime을 Time.unscaledDeltatime으로 바꾸고 해결하였다.

https://jcdevelop98.tistory.com/292

 

스파르타 Unity 8기 3일차 TIL

구현한 것들 간단한 오브젝트 풀링을 구현하여 터치 이펙트와 카드 파괴 이펙트를 구현하였다. using System.Collections; using System.Collections.Generic; using UnityEngine; public class ObjectPooler : MonoBehaviour { public

jcdevelop98.tistory.com

IEnumerator FadeIn()
{
    float alpha = 1.0f;
    while (alpha > 0)
    {
        alpha -= Time.deltaTime * fadeSpeed;
        fadeImage.color = new Color(0, 0, 0, alpha);
        yield return null;
    }

    OnLoadSceneByIndex(SceneManager.GetActiveScene().buildIndex);
}

IEnumerator FadeOut(int sceneIndex)
{
    if (isFadingOut)
        yield break;

    isFadingOut = true;

    float alpha = 0.0f;
    while (alpha < 1)
    {
        alpha += Time.deltaTime * fadeSpeed;
        fadeImage.color = new Color(0, 0, 0, alpha);
        yield return null;
    }

    SceneManager.LoadScene(sceneIndex);

    StartCoroutine(FadeIn());
}
IEnumerator FadeIn()
{
    float alpha = 1.0f;
    while (alpha > 0)
    {
        alpha -= Time.unscaledDeltaTime * fadeSpeed;
        fadeImage.color = new Color(0, 0, 0, alpha);
        yield return null;
    }

    OnLoadSceneByIndex(SceneManager.GetActiveScene().buildIndex);
}

IEnumerator FadeOut(int sceneIndex)
{
    if (isFadingOut)
        yield break;

    isFadingOut = true;

    float alpha = 0.0f;
    while (alpha < 1)
    {
        alpha += Time.unscaledDeltaTime * fadeSpeed;
        fadeImage.color = new Color(0, 0, 0, alpha);
        yield return null;
    }

    SceneManager.LoadScene(sceneIndex);

    StartCoroutine(FadeIn());
}

 

 

오늘의 회고

 오늘까지 기본 게임 로직을 거의 완성했다. 우여곡절이 많았지만 팀원 분들이 열심히 해주셔서 대충 거의 다 온 것 같다. 남은 건 추가 구현들과 버그 잡기만 남았다.

 내일은 아이템 구현과 최고점수 갱신 연결을 하려고 한다. 목요일까지 열심히 하자. 내일도 파이팅!

오늘의 학습 키워드

사운드 매니저

 

공부한 내용

사운드 매니저

BGM과 SFX를 나누어 사운드 매니저를 구현하였습니다.

음악 이름을 키 값으로 받고 value를 clip으로 받는 딕셔너리를 생성해 관리하도록 했습니다.

세팅 쪽 UI와 연결하여 음량이 변경되면 볼륨이 적용되도록 했습니다.

using System.Collections.Generic;
using UnityEngine;



public enum BGM
{
    MainMenu,
    InGame,
}

public enum SFX
{
    Break,
    Dead,
    LifeDown,
    OnHitBlock1,
    OnHitBlock2,
    OnHitBar,
}

public class SoundManager : MonoBehaviour
{
    public enum Sounds
    {
        BGM,
        SFX,
        MaxCount,
    }

    private static SoundManager _instance;
    public static SoundManager Instance { get => _instance; }

    private AudioSource[] _audioSources = new AudioSource[(int)Sounds.MaxCount];

    private const string BGM_PATH = "Sounds/BGM";
    private const string SFX_PATH = "Sounds/SFX";
    public Dictionary<string, AudioClip> ClipDict { get; } = new Dictionary<string, AudioClip>();

    void Awake()
    {
        if (_instance == null)
        {
            _instance = this;
        }
        
        DontDestroyOnLoad(gameObject);
    }

    private void Start()
    {
        Init();
        PlayBGM(BGM.InGame);
    }

    private void Init()
    {
        for (int i = 0; i < (int)Sounds.MaxCount; i++)
        {
            _audioSources[i] = gameObject.AddComponent<AudioSource>();
        }
        AudioClip[] bgms = Resources.LoadAll<AudioClip>(BGM_PATH);

        foreach (AudioClip clip in bgms)
        {
            ClipDict.Add(clip.name, clip);
        }

        AudioClip[] sfxs = Resources.LoadAll<AudioClip>(SFX_PATH);

        foreach (AudioClip clip in sfxs)
        {
            ClipDict.Add(clip.name, clip);
        }
    }

    public void PlayBGM(BGM bgm)
    {
        AudioSource audioSource = _audioSources[(int)Sounds.BGM];
        if (audioSource.isPlaying)
        {
            audioSource.Stop();
        }

        audioSource.clip = ClipDict[bgm.ToString()];

        audioSource.Play();
    }

    public void PlaySFX(SFX sfx)
    {
        AudioSource audioSource = _audioSources[(int)Sounds.SFX];
        if (audioSource.isPlaying)
        {
            audioSource.Stop();
        }

        audioSource.PlayOneShot(ClipDict[sfx.ToString()]);
    }

    public void SetBGMVolume(float value)
    {
        _audioSources[(int)Sounds.BGM].volume = value;
    }

    public void SetSFXVolume(float value)
    {
        _audioSources[(int)Sounds.SFX].volume = value;
    }
}

 

 

오늘의 회고

 오늘은 사운드 매니저와 리소스 연결 작업을 진행했다. 사운드 매니저는 다른 팀원 분이 하다가 내가 맡게 되었는데 내가 필요로 했던 부분을 미리 생각해놓고 있어서 그리 어려운 작업은 아니었던 것 같다. 우리가 만든 게임은 벽돌깨기 게임이고 아주 고전적인 게임이라서 리소스 찾는 작업에서 오래 걸리진 않았다. CC0인 에셋들도 존재해 사용할 수 있어서 다행이었다.

 내일은 아마 미리 먼저 아이템 쪽 구현을 하는 방향으로 생각 중이다. 아니면 다른 팀원분들한테 양해를 구하고 기본적인 로직을 짜는 것을 돕는 것도 고려중이다. 소통이 중요하니 어느 방향이든 팀에 도움이 되는 쪽으로 진행했으면 좋겠다. 내일도 파이팅!

오늘의 학습 키워드

Visual Studio 자동완성, Awake vs OnEnable

 

공부한 내용

Visual Studio 자동완성

Edit - Preferences - External Script Editor의 항목이 internal로 뜨며 자동완성이 안될 때가 있다.

Browse를 눌러 Visual Studio 경로로 지정을 해보았으나 실패했다.

이번 경우는 Visual Studio Editor가 설치가 되어있지 않아서 생기는 문제였다.

패키지 설치 후 스크립트를 껐다 켰더니 되는 모습!

 

 

Awake vs OnEnable

UI매니저는 Awake에서 초기화하는 싱글턴이고 다른 스크립트는 OnEnable에서 이벤트 등록을 했는데 UI 매니저의 싱글턴 인스턴스가 null이라는 오류가 나왔다.

왜 Awake에서 초기화를 진행했는데 OnEnable에서 null일까 이상했다.

그래서 확인을 해보려고 싱글턴의 Awake와 다른 스크립트의 OnEnable에 중단점을 찍어봤더니 다른 스크립트의 OnEnable이 먼저 불린 경우였다.

이유를 알기 위해 검색을 좀 해봤더니 OnEnable동일한 스크립트에서만 Awake 뒤에 동작하는 것을 보장하고 다른 것은 그렇지 않다는 것이었다. Execution Order를 이용하여 호출 순서를 지정하거나 커스텀 메서드를 생성하여 싱글턴의 Awake가 불렀는지 체크하고 그 이후에 동작하도록 만들 수 있지만 Execution Order는 다른 스크립트도 꼬일 수 있어서 하지 않았고 커스텀 메서드는 지금 방식에서 그냥 Start를 이용하는 게 간편하다고 생각해서 Start로 옮겨 해결하게 되었다.

해결된 모습

 

 

오늘의 회고

 오늘은 팀 과제 발제가 있는 날이었다. 팀원들과 고전게임을 만들어 보는 과제였는데 우리는 닷지, 똥피하기, 벽돌깨기 중 벽돌깨기가 가장 안 해본 프로젝트에 가까워서 선택하기로 했다. 와이어 프레임도 그리고 필수 구현 기능과 추가 구현 기능으로 나눠서 필수 구현부터 팀원들과 분배하여 구현하였다. 오늘 필수 구현은 다 진행하였고 겹치는 부분을 제외하고서는 머지까지 완료했다. 협업이란 게 잘 되고 시너지가 잘 나서 능률이 올라가는 것 같아서 좋았고 재밌었다.

 이렇게만 계속 구현을 한다면 기획을 했던 것들을 다 구현할 수 있을 것 같아서 기대가 된다. 다음 주도 파이팅!

 

 

참고 :

https://forum.unity.com/threads/execution-order-of-scripts-awake-and-onenable.763688/

 

Execution Order of Scripts - Awake and OnEnable

I am facing issues related to scripts execution order and this point is out of my mind though I am an experienced, Unity developer. So I expect a...

forum.unity.com

https://docs.unity3d.com/ScriptReference/MonoBehaviour.Awake.html

 

Unity - Scripting API: MonoBehaviour.Awake()

Awake is called either when an active GameObject that contains the script is initialized when a Scene loads, or when a previously inactive GameObject is set to active, or after a GameObject created with Object.Instantiate is initialized. Use Awake to initi

docs.unity3d.com

https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnEnable.html

 

Unity - Scripting API: MonoBehaviour.OnEnable()

Success! Thank you for helping us improve the quality of Unity Documentation. Although we cannot accept all submissions, we do read each suggested change from our users and will make updates where applicable. Close

docs.unity3d.com

 

+ Recent posts