Unity로 2D 게임을 만든다면, 아래의 샘플 프로젝트를 한 번 살펴보고 필요한 것만 적용해보는 것도 나쁘지 않을 듯하다.
1. RandomAudioPlayer
해당 스크립트를 사용하면 사용자가 지정한 클립 목록에서 선택한 임의의 사운드를 재생할 수 있습니다. 이 스크립트는 엘렌(플레이어 캐릭터) 프리팹에서 다양한 사운드를 제어하는 데 사용되는 것을 볼 수 있습니다(엘렌의 SoundSources 하위 게임 오브젝트 아래에서 확인). 예를 들어 풋스텝 소스를 보면 풋스텝이 무작위로 선택하는 네 가지 풋스텝 사운드 목록을 정의하고 피치 무작위화를 추가한 것을 볼 수 있습니다. 이 예시에는 특정 타일별 오버라이드 사용도 포함되어 있는데, 외계인 타일에 대한 오버라이드는 엘렌이 잔디 위를 걸을 때와는 다른 발자국 소리를 재생합니다. 타일맵이 아닌 게임 오브젝트에 오버라이드를 추가하려면 아래의 AudioSurface 섹션을 확인하세요.
Scripting side
스크립팅 측면에서 RandomAudioPlayer에는 사운드가 재생되어야 할 때 다른 스크립트에서 호출되는 PlayRandomSound 함수가 있습니다. (예: 발자국 소리는 PlayerController의 PlayFootstep 함수에 의해 트리거되고 함수 자체는 발이 땅에 닿는 프레임의 애니메이션 클립에서 호출됩니다). 이 함수는 타일베이스를 받아 오버라이드 사운드를 선택할 수 있습니다(예: 발소리는 플레이어가 현재 있는 표면을 사용하거나 총알이 충돌할 때 방금 충돌한 표면의 타일을 사용).
AudioSurface
오디오 표면은 타일맵이 아닌 모든 게임 오브젝트에 추가할 수 있는 스크립트입니다. 오디오 플레이어가 오버라이드 사운드를 찾을 때 어떤 타일을 사용할지 정의할 수 있습니다(예: 돌 상자는 외계인 타일을 타일 설정으로 사용하는 오디오 표면을 사용하므로 그 위를 걸으면 일반적인 “땅” 소리가 아닌 “돌 발자국” 소리가 나도록 합니다).
2.VFXController
VFX Controller는 프로젝트 폴더(리소스 폴더 내)에 있는 에셋으로, 게임에서 사용하는 모든 VFX를 나열할 수 있습니다. 이 에셋은 사용할 VFX의 인스턴스 풀을 생성하여 사용할 때마다 대신 게임 시작 시 VFX 프리팹 인스턴스화하는 데 드는 비용을 이동합니다.
그런 다음 PlayerController 또는 StateMachineBehaviour TriggerVFX와 같은 스크립트가 해당 VFX를 이름으로 트리거합니다. 오디오 플레이어와 마찬가지로 타일별로 오버라이드를 정의할 수도 있습니다. 예를 들어 발자국이 착지하는 표면에 따라 각 발자국에 사용되는 VFX에 대한 오버라이드를 설정할 수 있습니다. 이 프로젝트와 함께 제공되는 VFXController의 경우, 플레이어가 돌 위를 걸을 때 오버라이드를 사용하는 DustPuff VFX에서 그 예시를 확인할 수 있습니다.
오버라이드 타일은 VFX를 트리거하는 스크립트에 의해 지정됩니다(예: 발자국이 현재 표면을 통과하거나 표면에 부딪힌 총알이 해당 표면에 있는 타일을 통과하는 경우). 타일맵이 아닌 게임 오브젝트에서도 오버라이드 타일을 설정할 수 있으며, 게임 오브젝트가 어떤 오버라이드에 대응해야 하는지 설정하는 방법은 사운드 문서 Sounds.txt를 참조하세요.
3.Data Persistence
데이터 지속성 시스템을 사용하면 플레이하는 동안 일부 데이터를 저장하여 플레이어가 이미 수행한 작업에 대한 정보를 유지할 수 있습니다.
이 기능이 없으면 각 구역은 새로운 씬이므로 구역에 들어가면 해당 구역의 '기본' 상태(에디터에서 설계된 대로)가 로드되지만 플레이어가 수행한 모든 영구 동작(열쇠나 무기 잡기 등)은 실행 취소됩니다.
Usage in Editor
데이터 시스템은 IDataPersister라는 스크립팅 인터페이스를 통해 작동합니다. 이 인터페이스를 구현하는 오브젝트(예: 내장된 InventoryItem, HubDoor, PlayerInput 등)는 PersistentDataManager에서 데이터를 쓰고 읽을 수 있습니다. 이 인터페이스를 구현하는 모노비헤이비어는 인스펙터 하단에 데이터 설정 폴드아웃이 표시됩니다. 설정은 다음과 같이 구성됩니다:
- Data Tag: 관리자가 해당 게임 오브젝트에 데이터를 연결할 때 사용하는 게임 오브젝트의 고유 식별자입니다. 일부 기본 제공 컴포넌트는 자동 생성된 고유 ID를 사용하지만, “Zone_3_key” 또는 “Quest_Item_Card”와 같이 수동으로 입력한 이름일 수도 있습니다.
- Persistence Type: 데이터 지속성에는 네 가지 유형이 있습니다:
- Don't Persist: 지속성을 비활성화할 수 있습니다. 씬 변경 시 리셋해야 하는 게임 오브젝트에 유용합니다(예: 레벨을 다시 시작할 때 문을 다시 닫고 싶을 수 있습니다).
- Read Only: 이 게임 오브젝트는 데이터 읽기만 가능하고 쓰기는 불가능합니다. 이를 사용하는 방법은 동일한 데이터 태그가 있는 쓰기 전용 게임 오브젝트(아래 참조)를 사용하는 것입니다. 이 게임 오브젝트는 다른 게임 오브젝트가 해당 태그에 대해 쓰는 데이터를 사용하지만 그 위에 쓸 수는 없습니다.
- Write Only: 이 게임 오브젝트는 데이터를 쓸 수는 있지만 읽을 수는 없습니다. 사용 예는 위의 읽기 전용을 참조하십시오.
- Read Write: 가장 일반적인 사용 사례입니다. 게임 오브젝트는 지정된 데이터 태그가 있는 데이터를 읽고 씁니다.
Data Saving/Loading cycle
SaveData는 씬이 전환되기 전에 씬의 모든 IDataPersister 인스턴스에서 호출됩니다. LoadData는 새 씬이 로드된 후 씬의 모든 IDataPersister 인스턴스에서 호출됩니다. 언제든지 모든 IDataPersister에서 PersistentDataManager.SetDirty(this)를 호출하여 수동으로 데이터를 저장할 수 있습니다.
Example of use in code
데이터 관리자를 코드에서 사용할 수 있는 방법은 활성화/시작될 때 관련 데이터를 읽고 이에 반응하는 것입니다. 예를 들어 인벤토리 항목은 활성화 여부에 관계없이 해당 상태를 영구 데이터에 기록합니다. 따라서 장면이 로드되면 인벤토리 항목은 해당 태그에 연결된 데이터를 검색합니다. 잘못된 값이 저장되면 게임 오브젝트가 이미 검색되었으므로 스스로 비활성화될 수 있음을 의미합니다. 또 다른 예로는 문이 상태를 저장하는 경우를 들 수 있습니다. 씬이 로드되고 데이터를 읽으면 원하는 상태(예: 열림/닫힘)로 스스로 설정할 수 있습니다.
4.SceneLinkedSMB
SceneLinkedSMB는 StateMachineBehaviour의 작동 방식을 확장하는 클래스입니다. 애니메이터 스테이트 머신의 상태에 대한 비헤이비어가 모노비헤이비어에 대한 참조를 유지할 수 있도록 합니다. 특정 모노비헤이비어를 참조해야 하는 곳이면 어디에서나 SceneLinkedSMB를 사용할 수 있지만, 로직과 기능을 분리하는 아이디어로 설계되었습니다. 애니메이터 컨트롤러에는 실행 흐름을 제어하는 데 이상적인 스테이트 머신이 포함되어 있습니다. SceneLinkedSMB를 사용하면 장면 참조를 더 쉽게 얻을 수 있으므로 모노비헤이비어에서 공용 함수를 호출하여 기능을 제어할 수 있습니다. 이러한 방식으로 SceneLinkedSMB는 스테이트 머신의 일부로서 로직을 제어할 수 있으며, 연결된 모노비헤이비어는 기능을 제어할 수 있습니다.
Usage of SceneLinkedSMB
SceneLinkedSMB를 사용하여 새 비헤이비어를 코딩하려면 다음을 수행해야 합니다:
- 새 클래스가 SceneLinkedSMB를 상속하도록 합니다. 클래스는 Generic이므로 클래스가 보유해야 하는 MonoBehaviour의 유형을 지정해야 합니다. 예: public class EnemyAttackState: SceneLinkedSMB<Enemy>
- 해당 상태를 사용하여 애니메이터가 있는 모든 게임 오브젝트에서 초기화합니다. 위의 예제에서는 Enemy 클래스의 Start() 함수에 다음 줄을 추가할 수 있습니다: SceneLinkedSMB<Enemy>.Initialise(animator, this); 이 예제에서 animator는 SceneLinkedSMB<Enemy>가 켜져 있는 상태의 애니메이터에 대한 참조입니다. 여러 상태에 대해 10개의 서로 다른 스크립트가 있는 경우에도 이 함수는 한 번만 호출하면 됩니다. 모두 SceneLinkedSMB<Enemy>를 상속하기만 하면 모두 초기화됩니다.
- 필요한 상태 함수를 재정의하세요. 기능 발생 시기를 보다 구체적으로 제어할 수 있도록 더 많은 함수가 SceneLinkedSMB에 추가되었습니다. 클래스에서 클래스의 일반 유형 파라미터와 동일한 유형을 가진 m_MonoBehaviour라는 protected 멤버에 액세스할 수 있습니다. 따라서 예제를 계속 진행하면 m_MonoBehaviour는 Enemy 유형이 됩니다.
public class Enemy: MonoBehaviour
{
Animator animator;
void Start()
{
animator = GetComponent<Animator>();
SceneLinkedSMB<Enemy>.Initialise(animator, this);
}
public void TrickerAttack()
{
// ...
}
}
public class EnemyAttackState: SceneLinkedSMB<Enemy>
{
public override void OnSLStateEnter (Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
// m_MonoBehaviour is of type Enemy
m_MonoBehaviour.TrickerAttack ();
}
}
5.Object Pooling in the Gamekit
GameKit는 일부 시스템에 확장 가능한 오브젝트 풀링 시스템을 사용합니다. 다음 설명은 자체 용도로 시스템을 확장하려는 사용자에게만 해당되며, GameKit 작업에 필요한 지식은 아닙니다. 오브젝트 풀 시스템을 확장하려면 ObjectPool에서 상속하는 클래스와 PoolObject에서 상속하는 클래스 두 개를 생성해야 합니다. ObjectPool에서 상속하는 클래스는 풀 자체이고 PoolObject에서 상속하는 클래스는 풀의 각 프리팹에 대한 래퍼입니다. 두 클래스는 제네릭 타입으로 연결됩니다. ObjectPool과 PoolObject는 동일한 두 개의 제네릭 유형, 즉 ObjectPool에서 상속하는 클래스와 PoolObject에서 상속하는 클래스를 순서대로 가져야 합니다. 이는 예제를 통해 가장 쉽게 알 수 있습니다:
public class SpaceshipPool: ObjectPool<SpaceshipPool, Spaceship>
{
…
}
public class Spaceship: PoolObject<SpaceshipPool, Spaceship>
{
…
}
이는 풀이 포함된 객체의 유형을 알고 객체는 자신이 속한 풀의 유형을 알기 위한 것입니다. 이러한 클래스는 선택적으로 세 번째 제네릭 유형을 가질 수 있습니다. 이는 풀에서 풀 오브젝트를 가져올 때 호출되는 풀 오브젝트의 WakeUp 함수에 대한 매개변수를 갖고자 하는 경우에만 필요합니다. 예를 들어 우주선이 깨어날 때 연료의 양을 알아야 할 수 있으므로 다음과 같이 float 타입을 추가로 가질 수 있습니다:
public class SpaceshipPool: ObjectPool<SpaceshipPool, Spaceship, float>
{
…
}
public class Spaceship: PoolObject<SpaceshipPool, Spaceship, float>
{
…
}
기본적으로 풀 오브젝트에는 다음과 같은 필드가 있습니다:
- inPool: 풀 오브젝트가 현재 풀에 있는지 또는 깨어 있는지 여부를 결정하는 부울입니다.
- instance: 이 게임 오브젝트는 이 풀 오브젝트가 래핑하는 인스턴스화된 프리팹입니다.
- objectPool: 이 풀오브젝트가 속한 오브젝트 풀입니다. 이 클래스의 ObjectPool 유형과 동일한 유형을 가집니다.
풀 오브젝트에는 다음과 같은 가상 함수도 있습니다:
- SetReferences: 풀오브젝트가 처음 생성될 때 한 번 호출됩니다. 이 함수의 목적은 참조를 캐시하여 풀 오브젝트가 깨어날 때마다 참조를 수집할 필요가 없도록 하는 것이지만, 다른 일회성 설정에도 사용할 수 있습니다.
- WakeUp: 풀 오브젝트가 깨어나 풀에서 수집될 때마다 호출됩니다. 이 함수의 목적은 풀 오브젝트를 사용할 때마다 필요한 설정을 수행하는 것입니다. 클래스에 세 번째 일반 파라미터가 주어지면 해당 유형의 파라미터로 WakeUp을 호출할 수 있습니다.
- Sleep: 풀 오브젝트가 풀로 반환될 때마다 호출됩니다. 이 함수의 목적은 풀 오브젝트가 사용된 후 필요한 정리를 수행하는 것입니다.
- ReturnToPool: 기본적으로 단순히 풀 오브젝트를 풀로 반환하지만 추가 기능이 필요한 경우 재정의할 수 있습니다.
오브젝트 풀은 모노비헤이비어이므로 게임 오브젝트에 추가할 수 있습니다. 기본적으로 다음과 같은 필드가 있습니다:
- Prefab: 풀을 생성하기 위해 여러 번 인스턴스화되는 프리팹에 대한 참조입니다.
- InitialPoolCount: 시작 메서드에서 생성되는 풀 오브젝트 수입니다.
- Pool: 풀 오브젝트의 목록입니다.
ObjectPool에는 다음과 같은 함수도 있습니다:
- Start: 초기 풀 생성이 이루어지는 곳입니다. ObjectPool에 Start 함수가 있는 경우 기본 클래스 버전을 효과적으로 숨긴다는 점에 유의해야 합니다.
- CreateNewPoolObject: 이 함수는 풀 오브젝트가 생성될 때 호출되며 해당 SetReferences 함수를 호출한 다음 Sleep 함수를 호출합니다. 가상이 아니므로 재정의할 수 없지만 보호되므로 원하는 경우 상속 클래스에서 호출할 수 있습니다.
- Pop: 풀에서 풀 오브젝트를 가져올 때 호출됩니다. 기본적으로 inPool 플래그가 true로 설정된 첫 번째 객체를 검색하여 반환합니다. 참인 개체가 없으면 새 개체를 생성하고 반환합니다. 반환될 풀오브젝트에서 WakeUp을 호출합니다. 이것은 가상이므로 재정의할 수 있습니다.
- Push: 풀 오브젝트를 풀에 다시 넣을 때 호출됩니다. 기본적으로 inPool 플래그를 설정하고 풀 오브젝트에서 Sleep을 호출하지만 가상이므로 재정의할 수 있습니다.
객체 풀 시스템을 사용하는 방법에 대한 전체 예제는 BulletPool 문서 및 스크립트를 참조하세요.
BulletPool
BulletPool MonoBehaviour는 불릿 오브젝트 풀로, 각 오브젝트는 불릿 프리팹의 인스턴스를 감싸고 있습니다. BulletPool은 엘렌과 적 모두에게 사용되지만 각각 약간씩 다르게 사용됩니다. 엘렌의 경우 부모 게임 오브젝트에 불릿 프리팹을 프리팹 필드로 설정한 BulletPool MonoBehaviour가 첨부되어 있습니다. BulletPool의 다른 용도는 EnemyBehaviour 클래스입니다. 이 클래스는 GetObjectPool 정적 함수를 사용하여 BulletPool을 적극적으로 생성할 필요 없이 사용할 수 있습니다.
BulletPool 클래스에는 다음과 같은 필드가 있습니다:
- Prefab: 사용하려는 불릿 프리팹입니다.
- Initial Pool Count:풀에 처음 생성할 글머리글의 개수입니다.한 번에 사용할 것으로 예상되는 개수만큼 설정해야 합니다. 더 필요한 경우 런타임에 생성됩니다.
- Pool: 풀에 있는 불릿 오브젝트입니다. 인스펙터에는 표시되지 않습니다.
BulletPool 클래스에는 다음과 같은 함수가 있습니다:
- Pop: 풀에서 불릿 오브젝트 중 하나를 가져오는 데 사용합니다.
- Push:푸시: 풀 오브젝트를 풀에 다시 넣을 때 사용합니다.
- GetObjectPool:특정 프리팹이 주어지면 적절한 불릿풀을 찾는 정적 함수입니다.
풀에서 불릿을 가져올 때는 불릿 오브젝트 형태로 제공됩니다. BulletObject 클래스에는 다음과 같은 필드가 있습니다:
- InPool: 이 특정 글머리글이 풀에 있는지 또는 사용 중인지 여부입니다.
인스턴스: 인스턴스화된 프리팹입니다. - ObjectPool: 이 불릿 오브젝트가 속한 불릿풀에 대한 참조입니다.
- Transform: 트랜스폼 컴포넌트에 대한 참조입니다.
- Rigidbody2D: 인스턴스의 Rigidbody2D 컴포넌트에 대한 참조입니다.
- SpriteRenderer: 인스턴스의 스프라이트 렌더러 컴포넌트에 대한 참조입니다.
- Bullet: 인스턴스의 Bullet 스크립트에 대한 참조입니다.
불릿 오브젝트에는 다음과 같은 함수가 있습니다:
- WakeUp: Pop 함수가 호출될 때 불릿풀에 의해 호출됩니다.
- Sleep: 푸시 함수가 호출될 때 불릿풀이 호출합니다.
- ReturnToPool: 특정 불릿을 끝냈을 때 호출되어야 합니다. 이 함수는 해당 불릿풀의 푸시 함수를 호출하므로 해당 불릿풀의 슬립 함수를 호출합니다.
6.Behaviour Tree
💡Note: 해당 시스템은 스크립팅 전용 시스템입니다. 스크립트 트리의 작동 방식과 사용법을 이해하려면 Unity 스크립트와 C#의 작동 방식에 대한 기초 지식이 필요합니다.
이름에서 알 수 있듯이 비헤이비어 트리는 동작을 코딩하는 트리입니다. 비헤이비어 트리는 GameKit에서 적과 보스 전투 시퀀스에 대한 AI의 동작을 제어하는 데 사용되었습니다.
행동 트리의 시각적 예시는 아래와 같습니다:
Theory
게임이 업데이트될 때마다 트리는 루트의 각 자식 노드를 거치고, 해당 노드에 자식이 있는 경우 더 아래로 내려가는 등의 “틱”을 수행합니다.
각 노드에는 연관된 작업이 있으며, 세 가지 상태 중 하나를 부모 노드에 반환합니다:
- Success: 노드가 작업을 성공적으로 완료했습니다.
- Failure: 노드가 작업에 실패했습니다.
- Continue: 노드가 아직 작업을 완료하지 못했습니다.
반환된 상태는 각 노드 부모에 따라 다르게 사용됩니다.
예를 들어,
- * Selector는 현재 노드가 실패 또는 성공을 반환하면 다음 자식을 활성화하고 계속을 반환하면 현재 노드를 활성 상태로 유지합니다.
- * Test 노드는 자식 노드를 호출하고 테스트가 참이면 자식 상태를 반환하고, 테스트가 거짓이면 자식 노드를 호출하지 않고 실패를 반환합니다.
Game Kit Implementation
GameKit에서 비헤이비어 트리가 사용되는 방식은 스크립트를 통해 이루어집니다. 다음은 매우 간단한 비헤이비어 트리의 예시입니다: 먼저 파일 맨 위에 using BTAI;을 추가해야 합니다.
Root aiRoot = BT.Root();
aiRoot.Do(
BT.If(TestVisibleTarget).Do(
BT.Call(Aim),
BT.Call(Shoot)),
BT.Sequence().Do(
BT.Call(Walk),
BT.Wait(5.0f),
BT.Call(Turn),
BT.Wait(1.0f),
BT.Call(Turn)
));
Tree 액션이 실행될 수 있도록 Update 함수에서 aiRoot.Tick()을 호출해야 하므로 aiRoot는 클래스에 멤버로 저장되어야 합니다. aiRoot.Tick() 함수에서 업데이트가 어떻게 작동하는지 살펴보겠습니다:
- 먼저 TestVisibleTarget 함수가 참을 반환하는지 테스트합니다. 참이면 계속해서 조준 및 발사 함수를 호출하는 자식 함수를 실행합니다.
- 테스트가 거짓을 반환하면 If 노드는 Failure를 반환하고 루트는 다음 자식으로 이동합니다. 이것은 첫 번째 자식을 실행하는 것으로 시작하는 시퀀스입니다.
- Walk 함수를 호출합니다. 이 함수는 Success를 반환하여 다음 자식을 활성으로 설정하고 실행하도록 시퀀스를 호출합니다.
- Wait 노드가 실행됩니다. 5초 동안 기다려야 하고 방금 첫 번째로 호출되었으므로 대기 시간에 도달하지 않았으므로 Continue를 반환합니다.
- 시퀀스는 활성 자식으로부터 계속 상태를 받기 때문에 활성 자식을 변경하지 않으므로 다음 업데이트 시 해당 자식부터 시작합니다.
- Wait 노드가 타이머에 도달할 만큼 충분히 업데이트되면 Success를 반환하여 시퀀스가 다음 자식으로 이동하도록 합니다.
Nodes List
Sequence
자식 노드를 차례로 실행합니다. 자식 노드가 반환되면:
- 성공: 시퀀스가 다음 프레임에서 다음 자식을 선택합니다.
- 실패: 시퀀스가 다음 프레임에서 첫 번째 자식으로 돌아갑니다.
- 계속: 시퀀스가 다음 프레임에서 해당 노드를 다시 호출합니다.
RandomSequence
호출될 때마다 자식 목록에서 무작위 자식을 실행합니다. 생성자에서 각 자식에 적용할 가중치 목록을 int 배열로 지정하여 일부 자식이 선택될 확률을 높일 수 있습니다.
Selector
하나의 자식이 Success를 반환할 때까지 모든 자식을 순서대로 실행한 다음 나머지 자식 노드를 실행하지 않고 종료합니다. 성공이 반환되지 않으면 이 노드는 실패를 반환합니다.
Call
항상 Success를 반환하는 지정된 함수를 호출합니다.
If
주어진 함수를 호출합니다.
True를 반환하면 현재 활성 자식을 호출하고 그 상태를 반환합니다.
그렇지 않으면 자식을 호출하지 않고 실패를 반환합니다.
While
주어진 함수가 참을 반환하면 계속을 반환합니다(따라서 다음 프레임에서 트리를 체크하면 이전 노드를 모두 평가하지 않고 해당 노드부터 다시 시작합니다). 자식 함수는 차례로 실행됩니다.
함수가 거짓을 반환하고 루프가 중단되면 Failure를 반환합니다.
Condition
이 노드는 주어진 함수가 참을 반환하면 성공, 거짓을 반환하면 실패를 반환합니다.
자식 결과에 의존하는 다른 노드(예: 시퀀스, 셀렉터)와 체인으로 연결할 때 유용합니다.\
Repeat
모든 자식 노드를 지정된 횟수만큼 연속적으로 실행합니다.
반환 카운트에 도달할 때까지 계속 실행하여 성공(Success)을 반환합니다.
Wait
지정된 시간(처음 호출된 시점부터)에 도달할 때까지 계속을 반환한 다음 성공(Success)을 반환합니다.
Trigger
지정된 애니메이터에서 트리거를 설정(또는 마지막 인수가 거짓으로 설정된 경우 트리거 설정 해제)할 수 있습니다. 항상 Success를 반환합니다.
SetBool
주어진 애니메이터에서 부울 파라미터의 값을 설정할 수 있습니다. 항상 성공 반환
SetActive 주어진 게임 오브젝트를 활성/비활성 상태로 설정합니다. 항상 성공을 반환합니다.
'둥지 > Unity' 카테고리의 다른 글
Unity 텍스처 포맷으로 인한 색상 손실 (1) | 2024.03.24 |
---|---|
Unity iOS 빌드 에러 failed because this command failed to write the following output (0) | 2024.01.27 |
Unity Debug Log 색상 넣기 (0) | 2023.12.22 |
Unity 렌더링 파이프라인 정리 (0) | 2023.07.23 |
Unity 스카이박스 머티리얼 동적으로 변경하는 방법 (0) | 2023.06.03 |