팁들에 대해서
- 이것들은 3~20명의 인원으로 구성된 작은팀의 프로젝트 경험을 기반으로 합니다.
- 구조, 재사용성, 명확성, 기타 등등 의 비용은 팀 크기와 프로젝트의 크기에 따라 비용 지불 여부가 결정 됩니다.
- 많은 팁들은 취향의 문제 입니다.
(하지만 여기에 나온 여러 팁들은 우열을 가리기 어려울 정도로 좋은 기술들 입니다.) - 몇개의 팁은 공식 Unity 개발에 위배 되는 것들입니다. 예를 들면, 몇개의 특수한 인스턴스를 위한 프리팹의 사용은 매우 Unity 에서 싫어하는 방식으로, 비용 또한 상당히 높습니다(이렇게 라도 prefab 들을 사용하는게 나을 때도 있습니다). 그러나 나는 이런 팁들이 미친 짓 같아 보여도 가끔 좋은 결과를 가지고 오는 경우도 보곤 합니다.
Process
__MainScene_Backup. 프리팹을 분기 하는 경우 분명하게 이름을 명시하는게 안전하게 생성하는 방법이다. (Prefabs 섹션을 참고)- 이렇게 만들면 각각의 scene 을 다시 만드는게 불필요해 집니다.
- 이렇게 만들면 보다 빠르게 로딩 합니다(대부분의 객체가 scene 에서 공유되는 경우)
- 이렇게 만들면 scene 을 합치는게 쉬워집니다.(Unity 의 새로운 text 기반 scene 은 너무 많은 데이터를 포함하고 있어 합치는게 비현실적으로 보입니다)
- 이렇게 만들면 데이터 레벨 수준에서 관리를 할 수 있습니다.
- 상속을 활용한 방식을 지원하지 않습니다.
- 당신이 정의 하는 inspector 컴포넌트는 필드 타입 수준에서 구성이 안되며, 오로지 클래스 타입 레벨만 지원합니다. 예 로, 만약 모든 게임 오브젝트가 SomeCoolType 필드타입 이고, inspector 에서 다르게 렌더링 하려 할 경우,당신은 모든 클래스들에 대해서 inspector 를 작성해야 합니다.
당신은 기본적으로 inspector 시스템을 다시 구현함으로써 이러한 문제를 해결 할 수 있습니다. reflection의 몇가지 기술을 사용하여 해결할 수 있으며, 자세한 방법은 문서의 끝에 나와 있습니다.
Scene Organisation
myObject = FindMyObjectInScene();
if (myObjet == null)
{
myObject = SpawnMyObject();
}
Art
- 스카이 박스들을 위한 사각형 표기.
- 그리드(격자).
- shader 테스트를 위한 다양한 평면 색상: 흰색, 검은색, 50% 회색, 적색, 녹색, 청색, 마젠타, 노랑, 청록색.
- shader 테스트를 위한 그라데이션: 검은색 에서 흰색, 적색에서 녹색, 적색에서 청색, 녹색에서 청색.
- 검은색과 흰색으로 된 체커보드(체커판).
- 부드럽고 거친 노멀맵.
- 빠른 테스트 scene 설정을 위한 라이팅 을 갖춤(프리팹).
Prefabs
- 한 곳에서 각각의 타입을 변경 할 경우
- scene 을 변경하지 않고 변경 할 경우
- 복제된 Player 프리팹이 있습니다.
- 복제된 거에 대한 이름을
__Player_Backup 로 변경합니다. Playerprefab 으로 생성한걸 바꿉니다.- 작업이 진행 될 경우 __Player_Backup 를 지웁니다
- 사람 1:
Player프리팹을 복제 합니다.- 이름을
__Player_WithNewFeature나__Player_ForPerson2 로 바꿉니다. - 복제된 걸로 변경을 하고, 커밋을 하여 [사람 2] 에게 제공합니다.
- 사람 2:
- 새로운 프리팹으로 변경 생성 합니다.
Player프리팹을 복제하고,__Player_Backup이라고 부릅니다.__Player_WithNewFeature인스턴스를 scene 으로 드래그 합니다.- 오리지날 Player 프리팹을 인스턴스에 드래그 합니다.
- 작업이 진행 될 경우
__Player_Backup와__Player_WithNewFeature 를 지웁니다.
Extensions and MonoBehaviourBase
예를 들면:
public void Invoke(Task task, float time)
{
Invoke(task.Method.Name, time);
}
//Defined in the common base class for all mono behaviours
public I GetInterfaceComponent<I>() where I : class
{
return GetComponent(typeof(I)) as I;
}
public static List<I> FindObjectsOfInterface<I>() where I : class
{
MonoBehaviour[] monoBehaviours = FindObjectsOfType<MonoBehaviour>();
List<I> list = new List<I>();
foreach(MonoBehaviour behaviour in monoBehaviours)
{
I component = behaviour.GetComponent(typeof(I)) as I;
if(component != null)
{
list.Add(component);
}
}
return list;
}
public static class CSTransform
{
public static void SetX(this Transform transform, float x)
{
Vector3 newPosition =
new Vector3(x, transform.position.y, transform.position.z);
transform.position = newPosition;
}
...
}
RequiredComponent) can be a pain. 때때로 강제적인 컴포넌트 의존(RequiredComponent 유발)은 고통이 될 수 있습니다. 예를들어, it makes it difficult to change components in the inspector 그것은 어려운 inspector 의 구성요소를 변경 할 수 있습니다(base type 이 같은 경우). As an alternative, the following extension of GameObject can be used when a component is required to print out an error message when it is not found. 대안으로, 확장된 GameObject 를 찾을때 컴포넌트가 발견되지 않은 경우 오류 메세지를 출력 하면 된다.public static T GetSafeComponent<T>(this GameObject obj) where T : MonoBehaviour
{
T component = obj.GetComponent<T>();
if(component == null)
{
Debug.LogError("Expected to find component of type "
+ typeof(T) + " but found none", obj);
}
return component;
}
Idioms
- 몇가지 해결책이 함께 잘 작동하지 않습니다. 한가지 해결챌을 사용하면 다른 해결책에 적합하지 않으며 한 방향으로 디자인을 강요 합니다.
- 몇가지 해결책을 사용하는게 어디로 가야 하는지 팀 멤버를 이해 시킵니다. 이것은 구조와 코드를 이해하기 쉽게 합니다. 실수를 하기 더 쉽도록 유도가 됩니다.
- Coroutines vs. state machines.
- 하위 프리팹들 vs. 연결된(링크) 프리팹들 vs. 전지 전능한 프리팹들.
- 데이터 분리 전략.
- 2D 게임에서 상태를 위해 스프라이트를 사용 하는 방법.
- 프리팹 구조화.
- Spawn 전략.
- 개체를 찾을 수 있는 방법: 타입 vs. 이름 vs. 태그 vs. 레이어 vs. 레퍼런스(링크).
- 그룹 개체들을 찾는 방법: 타입 vs. 이름 vs. 태그 vs. 레이어 vs. 레퍼런스(링크) 배열
- 오브젝트의 그룹들 찾기 vs 자체 등록 (싱글톤 객체?)
- 실행 순서 제어 (Unity 를 이용하여 실행 순서 제어 vs. yield 로직 vs. Awake / Start 와 Update / Late Update 의존 vs. 수동 메소드 vs. 모든 주문 아키텍쳐)
- 개체 / 위치 / 타겟 의 선택과 인 게임의 마우스: 선택 관리자 vs 로컬 자체 관리
- scene 변화와 데이터 간의 유지: PlayerPrefs 를 통해, 또는 새로운 scene 이 로드 될때 지워지지 않은 객체.
- 애니메이션 결합 방법 (블렌딩, 더하거나 레이어링(계층관리)).
Time
Time.DeltaTime 와 Time.TimeSinceLevelLoad 은 계정 별 일시 중지와 시간 scale 이 가능합니다. 이것을 사용하는데는 훈련이 필요로 하지만, 많은 부분에 대해 쉽게 제작 가능하며, 특히 다른 Time 의 일을 실행 하기 편합니다. (예로 interface 애니메이션과 게임 애니메이션 등)Spawning Objects
게임이 실행 중 일때 개체를 쉽게 찾을 수 있도록 scene 개체에 부모를 설정 합니다. 비어있는 게임 오브젝트를 사용하거나 behaviour 가 없는 싱글톤을 이용하여 만들면 코드에 접근하기 쉽습니다. 이 오브젝트를 DynamicObjects 라고 부릅니다.
Class Design
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
protected static T instance;
/**
Returns the instance of this singleton.
*/
public static T Instance
{
get
{
if(instance == null)
{
instance = (T) FindObjectOfType(typeof(T));
if (instance == null)
{
Debug.LogError("An instance of " + typeof(T) +
" is needed in the scene, but there is none.");
}
}
return instance;
}
}
}
ParticleManager 또는 AudioManager 또는 GUIManager 등이 있습니다.- 고유한 프리팹의 인스턴스를 위한 싱글톤은 사용하는걸 피하는고 관리하지 않는게 좋습니다(Player 같은). 이 원칙을 준수하지 않으면 상속 계층 구조를 복잡하게 하며, 그리고 특정 유형의 변경을 힘들게 합니다. 오히려
GameManager에서 참조를 유지하는게 좋습니다.(또는 다른 전지 전능한 클래스
) - 종종 클래스 외부에서 사용되는 public 변수 와 메소드 에 대한 static 속성과 메서드를 사용합니다.
GameManager.Instance.player 대신에GameManager.Player 으로 작성이 가능합니다.
(역자 : 이런 경우 [HideInspector] 를 변수 상위에 붙이면 아예 노출이 일어나지 않습니다)
public float __aVariable;
- 게임 상태 저장, 그리고
- 게임 상태 디버그
[Serializable]
PlayerSaveData
{
public float health; //public for serialisation, not exposed in inspector
}
Player
{
//... bookkeeping variables
//Don’t expose state in inspector. State is not tweakable.
private PlayerSaveData playerSaveData;
}
- 각각의 게임 클래스에 템플릿 클래스를 정의합니다. 예로, 적인 경우 우리는 또한 EnemyTemplate 라고 정의 합니다. 모든 차별화된 tweakable 들은 EnemyTemplate 에 저장합니다.
- 게임 로직 클래스 에는, 변수나 템플릿 타입을 정의 합니다.
- 적 프리팹을 만들고, 두개의 템플릿 프리팹
WeakEnemyTemplate,andStrongEnemyTemplate 을 만듭니다. - 로딩하거나 오브젝트를 생성하면, 정식 템플릿에 템플릿 변수를 설정 합니다.
public class BaseTemplate
{
...
}
public class ActorTemplate : BaseTemplate
{
...
}
public class Entity<EntityTemplateType> where EntityTemplateType : BaseTemplate
{
EntityTemplateType template;
...
}
public class Actor : Entity <ActorTemplate>
{
...
}
(역자 : 배열의 인덱스를 직접적으로 사용하는걸 줄이라는 의미 인거 같습니다)
public void SelectWeapon(int index)
{
currentWeaponIndex = index;
Player.SwitchWeapon(weapons[currentWeapon]);
}
public void Shoot()
{
Fire(bullets[currentWeapon]);
FireParticles(particles[currentWeapon]);
}
[Serializable]
public class Weapon
{
public GameObject prefab;
public ParticleSystem particles;
public Bullet bullet;
}
public void FireAttack()
{
/// behaviour
Fire(bullets[0]);
}
public void IceAttack()
{
/// behaviour
Fire(bullets[1]);
}
public void WindAttack()
{
/// behaviour
Fire(bullets[2]);
}
public void WindAttack()
{
/// behaviour
Fire(bullets[WeaponType.Wind]);
}
[Serializable]
public class Bullets
{
public Bullet FireBullet;
public Bullet IceBullet;
public Bullet WindBullet;
}
- 변수의 그룹에 대한 별도의 클래스를 정의 합니다.
그것들을 public 및 serializable 합니다. - 기본 클래스에서, 위와 같이 정의된 각각의 유형의 public 변수를 정의 합니다.
- 변수를 Awake 나 Start 에서 초기화 하면 안되며; 그것들은 serializable 이기 때문에, Unity 에서 잘 처리 해줄 것입니다.
- 당신은 정의된 변수가 연결되기 전에 초기화를 지정하는게 가능하다
[Serializable]
public class MovementProperties //Not a MonoBehaviour!
{
public float movementSpeed;
public float turnSpeed = 1; //default provided
}
public class HealthProperties //Not a MonoBehaviour!
{
public float maxHealth;
public float regenerationRate;
}
public class Player : MonoBehaviour
{
public MovementProperties movementProeprties;
public HealthPorperties healthProeprties;
}
Text
이 작업을 수행하는 방법에는 여러가지가 있습니다. 한가지 방법은 예를 들어, 기본값을 영어로 설정하고, 각 문자열에 대한 공개 문자열 필드에 텍스트 클래스를 정의 하는 것입니다. 다른 언어는 하위 클래스를 두고 언어에 맞추어 다시 초기화 하는 것입니다.
Testing and Debugging
구문 분석(컬러 코드, 멀티 뷰, 스크린샷 기록)을 하게 되면 로그를 디버깅 하는게 훨씬 쾌적 할 수 있습니다. 여기에 자세한 정보가 있습니다 here.
- 모든 아이템 언락
- 적 제거
- GUI 끄기
- 플레이어 무적
- 모든 게임 플레이 불가.
- 팀 구성원은 사고로 자신의 디버그 옵션을 커밋 하고 모두에게 영향을 미치기 않기 위함입니다.
- 디버그 옵션을 바꾸는 것은 scene 을 바꾸는게 아니기 때문입니다.
Documentation
- 레이어 사용 (충돌, 컬링, raycasting – essentially, 어떤 항목에 어떤걸 해야)
- 태그 사용.
- 레이어를 위한 GUI 깊이 (무엇을 통해서 표시해야)
- scene 설치
- Idiom 설정
- 프리팹 구조
- 애니메이션 레이어
Naming Standard and Folder Structure
Naming General Principles
- 무언가를 호출 합니다. bird 는 Bird 를 호출 합니다.
- 이름을 선택 할 때는 확연하고 기억될 수 있는 이름으로 선택합니다. 만약 당신이 마야 게임을 만든다고 해서, 레벨 이름을 QuetzalcoatisReturn 로 해서는 안됩니다.
- 일관성을 유지 하십시오. 이름을 선택할때, 기준에 충실해야 합니다.
- Use Pascal case, like this: ComplicatedVerySpecificObject. Do not use spaces, underscores, or hyphens, with one exception (see Naming Different Aspects of the Same Thing).
이와같이 파스칼 케이스를 사용합니다: ComplicatedVerySpecificObject. 공간( ), 언더바(_), 또는 하이푼(-), 한가지 예외와 함께 (같은 일에 다른 이름을 보게 될 경우) - 버전 번호를 사용하지 말고, 또는 단어로 진행 상태를 표시 하지 마십시오 (WIP, final).
- Do not use abbreviations: DVamp@W should be DarkVampire@Walk.
약어를 사용하지 마십시오: DVamp@W 는 DarkVampire@Walk .으로 표기 합니다 - 디자인 문서의 용어를 사용합니다: 사망 을 문서상 호출 하려면 Die 애니메이션을 부릅니다. DarkVampire@Die, 를 부르며 DarkVampire@Death 는 안됩니다.
- 구체적인 설명은 왼쪽에 표기 합니다: DarkVampire 는 좋은 방법이며 VampireDark 는 안좋은 방법입니다. PauseButton는 좋은 방법이며 ButtonPaused는 안좋은 방법입니다. 이것을 예로 들면, 모든 버튼이 단어 버튼으로 시작하면 inspector 에서 일시 정지 버튼을 찾기 쉬워집니다. [많은 사람들이 다른 방법을 더 선호하며, 때문에 시각적으로 보다 더 확실히 그룹화를 하여 생성합니다. 이름이 있지만 그룹화를 위한게 아니며, 폴더를 위한것 입니다. 이름이 안정적으로 빠르게 자리 잡고 할 수 있도록 동일한 유형의 객체를 구분하는 것 입니다.]
- 일부 이름은 순서를 형성합니다. 이름에 숫자를 사용하며, 예로 PathNode0,PathNode1 가 있습니다. 항상 시작은 1이 아닌 0으로 시작합니다.
- 순서를 필요로 하지 않는 일에 번호를 사용하지 마십시오. 예로 Bird0, Bird1,Bird2 는 Flamingo, Eagle, Swallow 라고 표기합니다.
- 임시 객체는 이중으로 접두사에 언더바를 넣습니다 __Player_Backup.
Naming Different Aspects of the Same Thing
- GUI 버튼 상태는 EnterButton_Active, EnterButton_Inactive
- 텍스쳐는 DarkVampire_Diffuse, DarkVampire_Normalmap
- 스카이 박스는 JungleSky_Top, JungleSky_North
- LOD 그룹은 DarkVampire_LOD0, DarkVampire_LOD1
Structure
Folder Structure
Materials
GUI
Effects
Meshes
Actors
DarkVampire
LightVampire
...
Structures
Buildings
...
Props
Plants
...
...
Plugins
Prefabs
Actors
Items
...
Resources
Actors
Items
...
Scenes
GUI
Levels
TestScenes
Scripts
Textures
GUI
Effects
...
Scene Structure
Cameras Dynamic Objects Gameplay Actors Items ... GUI HUD PauseMenu ... Management Lights World Ground Props Structure ...
Scripts Folder Structure
ThirdParty
...
MyGenericScripts
Debug
Extensions
Framework
Graphics
IO
Math
...
MyGameScripts
Debug
Gameplay
Actors
Items
...
Framework
Graphics
GUI
...
How to Re-implement Inspector Drawing
1. 당신의 모든 에디터를 위한 기본 클래스를 정의
BaseEditor<T> : Editor
where T : MonoBehaviour
{
override public void OnInspectorGUI()
{
T data = (T) target;
GUIContent label = new GUIContent();
label.text = "Properties"; //
DrawDefaultInspectors(label, data);
if(GUI.changed)
{
EditorUtility.SetDirty(target);
}
}
}
2. reflection 과 recursion 를 사용하여 draw 컴포넌트에 reflection 을 사용
public static void DrawDefaultInspectors<T>(GUIContent label, T target)
where T : new()
{
EditorGUILayout.Separator();
Type type = typeof(T);
FieldInfo[] fields = type.GetFields();
EditorGUI.indentLevel++;
foreach(FieldInfo field in fields)
{
if(field.IsPublic)
{
if(field.FieldType == typeof(int))
{
field.SetValue(target, EditorGUILayout.IntField(
MakeLabel(field), (int) field.GetValue(target)));
}
else if(field.FieldType == typeof(float))
{
field.SetValue(target, EditorGUILayout.FloatField(
MakeLabel(field), (float) field.GetValue(target)));
}
///etc. for other primitive types
else if(field.FieldType.IsClass)
{
Type[] parmTypes = new Type[]{ field.FieldType};
string methodName = "DrawDefaultInspectors";
MethodInfo drawMethod =
typeof(CSEditorGUILayout).GetMethod(methodName);
if(drawMethod == null)
{
Debug.LogError("No method found: " + methodName);
}
bool foldOut = true;
drawMethod.MakeGenericMethod(parmTypes).Invoke(null,
new object[]
{
MakeLabel(field),
field.GetValue(target)
});
}
else
{
Debug.LogError(
"DrawDefaultInspectors does not support fields of type " +
field.FieldType);
}
}
}
EditorGUI.indentLevel--;
}
private static GUIContent MakeLabel(FieldInfo field)
{
GUIContent guiContent = new GUIContent();
guiContent.text = field.Name.SplitCamelCase();
object[] descriptions =
field.GetCustomAttributes(typeof(DescriptionAttribute), true);
if(descriptions.Length > 0)
{
//just use the first one.
guiContent.tooltip =
(descriptions[0] as DescriptionAttribute).Description;
}
return guiContent;
}
3. Define new Custom Editors
[CustomEditor(typeof(MyClass))]
public class MyClassEditor : BaseEditor<MyClass>
{}
이론적으로 이 단계는 자동일 수 있지만, 난 아직 시도 해보지 않았습니다.
출처 원문
