Last active
July 30, 2025 04:41
-
-
Save Curookie/42c979a7de7d656ec8cf6b8c01ea0457 to your computer and use it in GitHub Desktop.
Unity 유니티 실무2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
유니티 실무1 - https://gist.github.com/Curookie/5de19e581eb54cff7d7b643408ba930c | |
------ Alt+P - 인스펙터에서 따로 창으로 빼는 단축키 ------ | |
Properties 버튼과 동일하게 특정 ScriptableObject나 관리 에디터 따로 뺄때 유용하다. | |
----- 유니티 에디터 컴파일 속도 향상 *개 꿀 팁* ------ | |
Edit > Project Settings > Editor > Enter Play Mode Settings - Enter Play Mode Options 과 Reload 옵션들 체크하면 | |
- Reload Domain (static 함수 호출하는게 있으면 체크) | |
- Reload Scene (씬 로드 필요시 체크) | |
현재 프로젝트(Assembly Definition 사용중) 체감상 속도 4배 이상 빨라짐. | |
----- spriteRenderer의 bounds.size는 Camera 의 size에 영향을 받는다. ------ | |
Overlay UI Camera에 Orthographic 에서 16:9 해상도에 Size를 5.4로 안하고 2.7로 설정하고 SpriteRenderer를 해당 카메라에 띄우면 | |
Sprite의 실제사이즈와 달리 bounds.size는 실제사이즈의 절반으로 처리됨. | |
----- Voronoi 와 Worley 용어 인지해놓으면 좋다. ------ | |
랜덤한 다각형 Voronoi Noise | |
랜덤한 모양 생성 Worley Noise | |
활용해서 무언가 하면 됨. | |
----- 에디터 코드 몇가지 팁 ------ | |
1. [Icon("Assets기반 경로")] 로 파일의 아이콘을 지정할 수 있다. | |
2. EditorGUIUtility.IconContent("d_TerrainInspector.TerrainToolSettings").image 이런식으로 유니티 기본 아이콘을 사용할 수 있다. key string 리스트는 검색해보면 나옴 | |
3. [MenuItem("이 스트링 안 마지막에 &#f") 이런 문자를 넣으면 커스텀윈도우 단축키 설정가능 & - Alt # - Shift f - F == ALT + SHIFT + F 설정됨. | |
----- List에서 인덱스까지 같이 쉽게 체크하는 방법 ----- | |
var others = animationClips | |
.Where((clip, index) => index != selectedClipIndex) | |
.ToList(); | |
이렇게하면 해당 인덱싱이 아닌 리스트만 가져올수있다. | |
----- A is B 변수명 ----- | |
c#문법 중에 이런식으로 처리하면 만약 A가 B를 상속받고있으면 B로 변환된 변수를 바로 가져다 쓸 수 있음 | |
변수명.블라블라 | |
if(newField is IntegerField intField) { | |
intFiled.블라블라 | |
} | |
----- A : B 상속, B : C 상속했을때 A에서 B의 base.OverrideFunc 이 아닌 C의 base.OverrideFunc을 받고싶을 때 | |
public class C | |
{ | |
public void SpeakRaw() => Console.WriteLine("C Base"); | |
public virtual void Speak() => SpeakRaw(); | |
} | |
public class B : C | |
{ | |
public override void Speak() | |
{ | |
Console.WriteLine("B"); | |
base.Speak(); // C.Speak() | |
} | |
} | |
public class A : B | |
{ | |
public override void Speak() | |
{ | |
Console.WriteLine("A"); | |
base.SpeakRaw(); // C의 본질 호출 가능 | |
} | |
} | |
----- 에디터 코드로 A 클래스 인스펙터에서 특정값을 할당했을때 B 클래스 인스펙터의 값에 특정값을 할당시키는 방법 (응용 하셈) | |
ex) ScriptableObject가 도면아이템, 무기아이템 두 개가 있는데 도면아이템의 특정값을 지정하면 무기아이템의 특정값이 자동할당되는 코드 | |
SerializedProperty weaponRefProp; | |
SO_Blueprint _blueprint; | |
void OnInspectorGUI() { | |
if(weaponRefProp.objectReferenceValue is SO_Weapon refWeapon) { | |
if(!refWeapon.GetRefBlueprintInfo()) { | |
var weaponSO = new SerializedObject(refWeapon); | |
var refBlueprintProp = weaponSO.FindProperty("_refBlueprintInfo"); | |
refBlueprintProp.objectReferenceValue = _blueprint; | |
weaponSO.ApplyModifiedProperties(); | |
} | |
} | |
} | |
----- c# 문법 별칭 정해서 함수가 중복일때 어떤 네임스페이스를 사용할지 정할 수 있는 방법 ------ | |
using System.Diagnostics; | |
using Debug = UnityEngine.Debug; | |
----- DOTween Editor에서 EditMode일때 애니메이션 미리보기 구현하는 코드 ----- | |
using DG.DOTweenEditor; | |
void RunToggleAnimationInEditMode() { | |
var tween_ = m_toggle.PlayClickAnimation(); | |
if(tween_ != null) { | |
DOTweenEditorPreview.PrepareTweenForPreview(tween_); | |
DOTweenEditorPreview.Start(); | |
} | |
} | |
에디터 코드에서 | |
if (GUILayout.Button("Test Toggle Animation")) { | |
if (!Application.isPlaying) { | |
RunToggleAnimationInEditMode(); | |
} | |
} | |
이런식으로 사용하면 플레이 안하고 쉽게 애니메이션 테스트 해볼 수 있다. | |
----- 진짜 정말 많이 쓰는 3가지 프레임워크 코드 공유 ----- | |
1. 트랜스 폼 아래에 모든 게임오브젝트 제거 | |
public static void DestroyAllChildren(this Transform tran) { | |
var children = new List<GameObject>(); | |
for (var i = 0; i < tran.childCount; i++) { | |
children.Add(tran.GetChild(i).gameObject); | |
} | |
var failsafe = 0; | |
while (children.Count > 0 && failsafe < 200) { | |
var child = children[0]; | |
GameObject.DestroyImmediate(child); | |
if (children[0] == child) { | |
children.RemoveAt(0); | |
} | |
failsafe++; | |
} | |
} | |
2. 해당 트랜스폼의 가장 상위 부모 캔버스 찾는 거 | |
public static Canvas GetTopmostCanvas(this Transform tran) { | |
Canvas[] parentCanvases = tran.GetComponentsInParent<Canvas>(); | |
if (parentCanvases != null && parentCanvases.Length > 0) { | |
return parentCanvases[parentCanvases.Length - 1]; | |
} | |
return null; | |
} | |
3. 스크린 값 (마우스 포인터) Vector2가 있을경우 World 좌표로 자동변환 단 변환할 좌표 트랜스폼을 제시 | |
public static Vector3? ScreenToWorld(this Vector2 screenPos_, Transform targetTrans_) { | |
var targetCanvas_ = targetTrans_.GetTopmostCanvas(); | |
Vector3? worldCursorPos_ = null; | |
if(targetCanvas_) { | |
if(targetCanvas_.renderMode == RenderMode.ScreenSpaceOverlay) { | |
worldCursorPos_ = screenPos_; | |
} else { | |
worldCursorPos_ = targetCanvas_.worldCamera.ScreenToWorldPoint(new Vector3(screenPos_.x, screenPos_.y, targetCanvas_.planeDistance)); | |
} | |
} else { } | |
return worldCursorPos_; | |
} | |
----- URP 2D, Overlay Camera에서 2D Light가 적용이 안되는 이슈 (2022.3.34f1)----- | |
이슈 : | |
메인 Base 캠은 1,2,3 레이어 Culling | |
Overlay 캠은 4 레이어를 Culling 했는데 | |
1번 레이어에 있는 2D Light - Global 효과가 | |
SortingLayer설정과 상관없이 Overlay캠에 적용된 4번 레이어에 적용이 안됨. | |
해결법 : 2D Light를 하나더 만들어서 2D Light 오브젝트 레이어를 같은 레이어(4번 레이어)로 변경시 됨. | |
----- 코드로 게임 오브젝트 생성시 ------ | |
new GameObject("Contents", typeof(CanvasRenderer), typeof(RectTransform)) | |
이런식으로 생성하면 컴포넌트도 쉽게 붙일 수 있다. | |
------ RectTransform 가져오는 다른 방법 ------ | |
보통 transform.GetComponent<RectTransform>() 으로 가져오는데 | |
transform as RectTransform 으로 가져올수도 있다. | |
(transform as RectTransform).블라블라 이런식도 가능 | |
------ 커스텀 디버거 Custom Debug ------ | |
[HideInCallstack] 콜백에 커스텀 디버깅 코드가 스태킹되서 콘솔 볼때 스트레스 받을 때 쓰는 어트리뷰트가 있었는데 2022.3 에서 지원 안한다. | |
그대신 콘솔 창에 햄버거 아이콘 누르고 Strip logging stacking 체크해주면 자동으로 커스텀 디버깅 스태킹이 사라짐. | |
[예시] | |
Common : AAAA | |
UnityEngine.Debug:Log (object) | |
GameFrameworks.DBug:<Log>g___Log|6_0 (GameFrameworks.DBug/<>c__DisplayClass6_0&) (at Assets/Plugins/GameFrameworks/Utility/DBug.cs:53) | |
GameFrameworks.DBug:Log (string,GameFrameworks.DBug/PrintLogType) (at Assets/Plugins/GameFrameworks/Utility/DBug.cs:42) | |
TokaTonTon.GameSceneManager:OnEnable () (at Assets/02_Script/Manager/GameSceneManager.cs:54) | |
UnityEngine.GUIUtility:ProcessEvent (int,intptr,bool&) | |
Common : AAAA | |
UnityEngine.Debug:Log (object) | |
TokaTonTon.GameSceneManager:OnEnable () (at Assets/02_Script/Manager/GameSceneManager.cs:54) | |
UnityEngine.GUIUtility:ProcessEvent (int,intptr,bool&) | |
------ Tilemap에서 반 칸짜리 타일(Half Tile) 구분할때 유용한 방법 ------ | |
이름을 텍스쳐 이름을 HALF_ 이런식으로 넣고 이름으로 구분하는거 유용하다. TilemapCollider2D 개별 체크안됨. | |
foreach (var position in bounds.allPositionsWithin) | |
{ | |
Vector3Int tilePosition = new Vector3Int(position.x, position.y, 0); | |
if (tilemap.HasTile(tilePosition)) | |
{ | |
bool isHalf = false; | |
Tile tile = tilemap.GetTile<Tile>(position); | |
if (tile) | |
{ | |
if (tile.name != null && tile.name.Contains("HALF")) | |
{ | |
isHalf = true; | |
} else if (tile.sprite != null && tile.sprite.name.Contains("HALF")) | |
{ | |
isHalf = true; | |
} | |
} | |
Vector3Int aboveTilePosition = new Vector3Int(tilePosition.x, tilePosition.y + (isHalf ? 0 : 1), 0); | |
if (!tilemap.HasTile(aboveTilePosition)||isHalf) | |
{ | |
Vector3 worldPosition = tilemap.CellToWorld(aboveTilePosition); | |
Vector3Int localPosition = tilemap.layoutGrid.WorldToCell(worldPosition); | |
walkableTiles.Add((Vector2Int)localPosition); | |
} | |
} | |
------ Post Processing 스크립트로 제어할 때 주의할 점 ------ | |
1. _dOF.focalLength.value = 0; | |
2. _dOF.focalLength = new ClampedFloatParameter(0, 1, 300, true); | |
두 가지 방안이 있는데 | |
이렇게 제어할 때는 반드시 min max 값이 inspector의 min max값과 동일해야한다. | |
아래방식대로 적용할때는 override가 필요할경우 즉시 반영해야할경우 아래 방식으로 true해서 처리해야함. | |
종류마다 1, 2 스크립팅을 다르게 해야할 필요가 있어보임. Depth of Field는 1번방식으로만 적용됨. (2022.3.34f1 기준) | |
------ Timeline Pasue 할때 이미 완료되서 Hold된 애니메이션 초기화 되는 현상 막는법 ----- | |
pD_Director.Pause(); | |
pD_Director.RebuildGraph(); | |
정지하고 직후 Graph를 Rebuild시켜준다. | |
단 이때 Animator의 Parameter값 현재 실행되고있던 State 가 모두 초기화 되므로 기존 Animator 상태를 저장하고 복구하는 코드도 포함해야함. | |
SaveAllAnimatorStates(); | |
pD_Director.Pause(); | |
pD_Director.RebuildGraph(); | |
pD_Director.Evaluate(); | |
RestoreAllAnimatorStates(); | |
이런 식으로 코드를 짜줘야한다. | |
Timeline 시작시에 Animation Track들의 모든 Aniamtor를 순회하며 Parameter를 저장하는 Dictionary를 한번 만들어두는것도 필수. | |
재실행은 특별한거 없이 Resume이면 됨. | |
public void RestartTimeline() { | |
pD_Director.Resume(); | |
DialogueController.Inst.OnDialogueEnd.RemoveListener(RestartTimeline); | |
} | |
EX) | |
private void SaveAllAnimatorStates() { | |
foreach (var animator in allAnimators) | |
{ | |
Dictionary<string, float> floatParams = new(); | |
Dictionary<string, int> intParams = new(); | |
Dictionary<string, bool> boolParams = new(); | |
Dictionary<int, float> layerWeights = new(); | |
AnimatorStateInfo currentState = animator.GetCurrentAnimatorStateInfo(0); | |
animatorStates[animator] = (currentState.fullPathHash, currentState.normalizedTime); | |
foreach (AnimatorControllerParameter param in animator.parameters) | |
{ | |
DBug.Log($"{param.name} {animator.GetInteger(param.name)}"); | |
switch (param.type) | |
{ | |
case AnimatorControllerParameterType.Float: | |
floatParams[param.name] = animator.GetFloat(param.name); | |
break; | |
case AnimatorControllerParameterType.Int: | |
intParams[param.name] = animator.GetInteger(param.name); | |
break; | |
case AnimatorControllerParameterType.Bool: | |
boolParams[param.name] = animator.GetBool(param.name); | |
break; | |
} | |
} | |
for (int i = 0; i < animator.layerCount; i++) | |
{ | |
layerWeights[i] = animator.GetLayerWeight(i); | |
} | |
animatorFloatParams[animator] = floatParams; | |
animatorIntParams[animator] = intParams; | |
animatorBoolParams[animator] = boolParams; | |
animatorLayerWeights[animator] = layerWeights; | |
} | |
} | |
------ CinemachineVirutalCamera Noise 안먹을 때! ---- | |
컴포넌트 달려있는 오브젝트를 복제해서 생성할 경우 Noise가 안먹는 상황이 발생했다. (2022.3.34f1 버전 기준) | |
Body 컴포넌트 None , Noise 컴포넌트 None으로 바꾼다음 다시 설정하면 제대로 먹힘. | |
------ 픽셀 게임 만들때 Texture 설정 ------- | |
PPU - 32나 64 | |
Filter Mode - Point | |
Compression - None | |
------ area.bounds.Contains() 2DCollider의 bounds.Contains 함수가 제대로 작동안될때 ------ | |
2DCollider.bounds.Contains(Vector3) 로 체크할때 분명히 범위안에 있는데 false가 뜨는경우 | |
z값을 2DCollider의 z값으로 설정하면 제대로 먹힘 | |
ex) | |
잘못된 예 - area&&target&&area.bounds.Contains(target.transform.position); | |
옳은 예 - area&&target&&area.bounds.Contains(new Vector3(target.transform.position.x, target.transform.position.y, area.transform.position.z)); | |
----- Awake보다 빨리 실행시키는 방법 ------ | |
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] | |
static void PreAwake() { | |
} | |
스태틱함수로 만들어야함. | |
----- 컴파일 제외 꿀팁 ------ | |
안쓰는 스크립트를 컴파일 제외 시키고싶을때 폴더 명 앞에 ~ 를 붙여주면 완전히 무시할 수 있다. | |
----- Master Audio BGM 팁 ----- | |
var track1Playing = PlaylistController.InstanceByName("BGM_Track1")?.ActiveAudioSource?.clip?.name ?? ""; | |
BGM 켜져 있는지 체크 | |
----- Assembly Definition 사용 시 매우매우 주의할점 ------ | |
기본적으로 Auto Referecnce 라는 체크박스가 켜져있는데 내가 일반적으로 컴파일안할거면 절대 꺼놔야한다!!! 켜놓으면 | |
그냥 기존에 컴파일하던대로 자동으로 컴파일 과정에 포함되서 쓰는 의미가 없다. | |
ex) example 붙은 Assembly Definition - Auto Reference 반드시 끄기 | |
+ Define Constraints에 UNITY_INCLUDE_TESTS 넣어서 제외 시키기 | |
+ Include Platforms에 QNX 이런거만 체크해서 제외 시키기 | |
----- 에디터에서 isPlaying이 아닐때 PlayMode 아닐때 tranform 을 코드로 바꿀경우 적용이 안될 수 있는데 이때 ---- | |
delayCall 로 변경하면 적용됨 | |
ex) | |
#if UNITY_EDITOR | |
EditorApplication.delayCall += () => { | |
sR_Main.transform.localPosition = new Vector3(Building.GetOffset.x, Building.GetOffset.y, 0); | |
}; | |
if(!Application.isPlaying) { | |
EditorUtility.SetDirty(sR_Main.gameObject); | |
SceneView.RepaintAll(); | |
} | |
#else | |
sR_Main.transform.localPosition = new Vector3(Building.GetOffset.x, Building.GetOffset.y, 0); | |
#endif | |
----- 유니티 프로젝트 에디터 버전 체크하는 법 ------ | |
ProjectSettings/ProjectVersion.txt 에서 확인 가능하다. | |
----- 에디터 코드 꿀팁 HelpBox 잘 활용하면 매우 쉽고 빠르게 인스펙터 이쁘게 꾸밀 수 있다. (기본 디자인이 둥근 사각형이라 좋음)------ | |
예시) Editor 코드 | |
protected void InitializeStyles() | |
{ | |
foldoutStyle = new GUIStyle(EditorStyles.foldout) | |
{ | |
fontStyle = FontStyle.Bold, | |
margin = new RectOffset(15, 0, 4, 4), | |
border = new RectOffset(1, 1, 1, 1), | |
}; | |
headerStyle = new GUIStyle(EditorStyles.helpBox) | |
{ | |
padding = new RectOffset(10, 10, 8, 8), | |
margin = new RectOffset(4, 4, 4, 4), | |
border = new RectOffset(1, 1, 1, 1), | |
}; | |
contentBoxStyle = new GUIStyle(EditorStyles.helpBox) | |
{ | |
padding = new RectOffset(10, 10, 5, 5), | |
margin = new RectOffset(0, 0, 4, 4), | |
border = new RectOffset(1, 1, 1, 1), | |
}; | |
vector2Style = new GUIStyle(EditorStyles.numberField) | |
{ | |
fixedWidth = 150, | |
fontSize = 12, | |
fontStyle = FontStyle.Bold | |
}; | |
originalBGColor = GUI.backgroundColor; | |
} | |
protected virtual void MainSettings() { | |
if (!stylesInitialized) { | |
InitializeStyles(); | |
stylesInitialized = true; | |
} | |
EditorGUILayout.PropertyField(_SO_Creature, new GUIContent("프리셋")); | |
DrawSpritePreview(); | |
GUI.backgroundColor = new Color(0.1f, 0.1f, 0.1f); | |
EditorGUILayout.BeginVertical(headerStyle); | |
GUI.backgroundColor = originalBGColor; | |
EditorGUILayout.BeginHorizontal(); | |
EditorGUILayout.LabelField("현재 속도:", GUILayout.Width(70)); | |
GUILayoutOption[] options = { GUILayout.Width(180) }; | |
velocity.vector2Value = EditorGUILayout.Vector2Field("", velocity.vector2Value, options); | |
GUILayout.FlexibleSpace(); | |
if (GUILayout.Button(isAllHide ? "Show All" : "Hide All", GUILayout.Width(70))) { | |
isAllHide = !isAllHide; | |
ToggleFoldouts(isAllHide); | |
} | |
EditorGUILayout.EndHorizontal(); | |
EditorGUILayout.EndVertical(); | |
GUI.backgroundColor = new Color(0.1f, 0.1f, 0.1f); | |
EditorGUILayout.BeginVertical(headerStyle); | |
GUI.backgroundColor = originalBGColor; | |
showBaseSettings = EditorGUILayout.Foldout(showBaseSettings, "기본", true, foldoutStyle); | |
if(showBaseSettings) { | |
EditorGUILayout.BeginVertical(contentBoxStyle); | |
DrawOptionalProperty(applyMass, mass, "무게", "1 - 10"); | |
DrawOptionalProperty(applyFriction, friction, "마찰력", "0 - 1"); | |
DrawOptionalProperty(applyBounce, bounciness, "반동력", "0 - 1"); | |
DrawOptionalProperty(applyPush, null, "밀림 여부", "False or True"); | |
EditorGUILayout.EndVertical(); | |
} | |
EditorGUILayout.EndVertical(); | |
GUI.backgroundColor = new Color(0.1f, 0.1f, 0.1f); | |
EditorGUILayout.BeginVertical(headerStyle); | |
GUI.backgroundColor = originalBGColor; | |
showDetailSettings = EditorGUILayout.Foldout(showDetailSettings, "세부 설정", true, foldoutStyle); | |
if(showDetailSettings) { | |
EditorGUILayout.BeginVertical(contentBoxStyle); | |
EditorGUILayout.PropertyField(serializedObject.FindProperty("gravityModifier"), new GUIContent("중력 가산치")); | |
EditorGUILayout.Space(10); | |
EditorGUILayout.PropertyField(serializedObject.FindProperty("minGroundNormalY"), new GUIContent("지면착지 판정각")); | |
EditorGUILayout.EndVertical(); | |
} | |
EditorGUILayout.EndVertical(); | |
} | |
----- 태그 비교 최적화 ----- | |
transform.gameObject.tag == "AA" 하는 것보다 transform.CompareTag("AA") 하는게 조금 더 이점이 있다. | |
----- collider.Cast 함수 ----- | |
collider의 가장자리에서 특정 방향 및 거리에 떨어진 Collider를 찾을 때 사용하면 유용하다. | |
ex) 콜라이더의 오른쪽 shellRadius 만큼 떨어진 영역 안에 있는 모든 콜라이더를 찾는 예제 | |
var colliders = body.GetComponents<Collider2D>(); | |
var hitBufferPush = new RaycastHit2D[16]; | |
int count = 0; | |
foreach (var collider in colliders) | |
{ | |
if (!collider.isTrigger) | |
{ | |
count += collider.Cast(Vector2.right, contactFilter, hitBufferPush, shellRadius); | |
} | |
} | |
----- 스크립트를 에디터에서 혹은 빌드 후 가장 먼저 실행시키는 법 (PlayMode 아닐때에도 실행) ----- | |
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] 태그로 빌드 이후에도 가장 먼저 실행가능 | |
에디터에서만 하려면 | |
#if UNITY_EDITOR | |
[InitializeOnLoadMethod] | |
#endif | |
ex) 현재 씬에 매니저씬이 없으면 자동으로 불러오려는 상황 예시 | |
#if UNITY_EDITOR | |
using UnityEditor; | |
using UnityEditor.SceneManagement; | |
#endif | |
using UnityEngine; | |
using UnityEngine.SceneManagement; | |
public class AutoLoadManagerScene : MonoBehaviour | |
{ | |
private const string ManagerSceneName = "Manager"; | |
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] | |
private static void LoadManagerSceneInPlayMode() | |
{ | |
if (!IsSceneLoaded(ManagerSceneName)) | |
{ | |
SceneManager.LoadSceneAsync(ManagerSceneName, LoadSceneMode.Additive); | |
Debug.Log($"Manager scene '{ManagerSceneName}' automatically loaded in Play Mode."); | |
} | |
} | |
#if UNITY_EDITOR | |
[InitializeOnLoadMethod] | |
private static void LoadManagerSceneInEditMode() | |
{ | |
EditorApplication.update += () => | |
{ | |
if (!Application.isPlaying && !IsSceneLoaded(ManagerSceneName)) | |
{ | |
EditorSceneManager.OpenScene($"Assets/01_Scene/{ManagerSceneName}.unity", OpenSceneMode.Additive); | |
Debug.Log($"Manager scene '{ManagerSceneName}' automatically loaded in Edit Mode."); | |
} | |
}; | |
} | |
#endif | |
private static bool IsSceneLoaded(string sceneName) | |
{ | |
for (int i = 0; i < SceneManager.sceneCount; i++) | |
{ | |
if (SceneManager.GetSceneAt(i).name == sceneName) | |
return true; | |
} | |
return false; | |
} | |
} | |
----- UI Toolkit 으로 .uss 스타일 시트를 만들 수 있다. ----- | |
Unity UI Toolkit 패키지로 | |
웹에서 css 처럼 에디터 상 커스텀 스타일을 쉽게 만들 수 있다. | |
----- Easy Save 3 파일 저장 ----- | |
ES3File은 Easy Save 3에서 제공하는 기능 중 하나로, 메모리 내에서 작동하는 저장 파일 객체 | |
이 객체는 파일을 직접적으로 다루기보다, 메모리 상에서 파일의 내용을 관리하고 수정한 후에 최종적으로 디스크에 저장하거나 로드하는 방식을 사용 | |
성능 최적화: 파일을 자주 읽고 쓰지 않고 메모리에서 작업을 처리함으로써 성능이 향상 | |
버퍼링 기능: 데이터를 수집한 뒤 한 번에 저장하거나 로드할 수 있어 효율적 | |
안정성: 데이터를 메모리에서 작업한 후에 문제가 발생할 경우 디스크로 저장되지 않으므로 안정성 높음 | |
ES3File _es3File = new ES3File(filePath, es3Settings); | |
_es3File.Save<string>(key, data); | |
_es3File.Sync(); | |
// 비동기로 데이터를 파일에 저장하기도 가능 | |
await _es3File.SyncAsync(); | |
------ uGUI Slider 키보드 제어 막기 ----- | |
Slider가 좌우 키제어(Horizontal and Vertical axes)를 받아서 예상치 못한 Slider Value값이 변경되어 UI가 바뀌는 현상이 있을 수 있다. | |
키보드 제어를 막으려면 Navigation 옵션을 None으로 두면 됨. | |
코드로만 제어하려면 (안전하게 마우스 제어도 막으려면) Interactable 도 false로 하자. | |
----- 절차적 콘텐츠 생성(Procedural Content Generation, PCG) ----- | |
랜덤 생성 알고리즘 | |
1. Cellular Automaton | |
- 2D Platformer 맵 생성시 사용할 수 있음. (동굴, 던전) | |
2. Dungeon Room 생성 알고리즘 (BSP) | |
Binary Space Partitioning (BSP 알고리즘) | |
- 던전과 같은 방을 구분하고 연결하는 데 사용 | |
- 맵 생성의 가장 기본적인 알고리즘 | |
using UnityEngine; | |
using System.Collections.Generic; | |
public class BSPNode | |
{ | |
public RectInt rect; // 노드가 차지하는 전체 영역 | |
public RectInt room; // 노드 내의 방 영역 | |
public BSPNode left; // 좌측 자식 노드 | |
public BSPNode right; // 우측 자식 노드 | |
public int minRoomSize = 5; // 방의 최소 크기 | |
public int maxRoomSize = 12; // 방의 최대 크기 | |
public BSPNode(RectInt rect) | |
{ | |
this.rect = rect; | |
} | |
// 현재 노드를 자식 노드로 분할 | |
public bool Split() | |
{ | |
// 가로 또는 세로로 분할을 결정 | |
bool splitHorizontally = Random.Range(0, 2) == 0; | |
// 분할 가능 여부 확인 | |
if (rect.width < minRoomSize * 2 && rect.height < minRoomSize * 2) | |
{ | |
return false; // 분할 불가능 | |
} | |
// 분할 방향에 따라 적절한 최대 길이를 계산 | |
if (splitHorizontally) | |
{ | |
if (rect.height < minRoomSize * 2) return false; | |
int splitY = Random.Range(minRoomSize, rect.height - minRoomSize); | |
left = new BSPNode(new RectInt(rect.x, rect.y, rect.width, splitY)); | |
right = new BSPNode(new RectInt(rect.x, rect.y + splitY, rect.width, rect.height - splitY)); | |
} | |
else | |
{ | |
if (rect.width < minRoomSize * 2) return false; | |
int splitX = Random.Range(minRoomSize, rect.width - minRoomSize); | |
left = new BSPNode(new RectInt(rect.x, rect.y, splitX, rect.height)); | |
right = new BSPNode(new RectInt(rect.x + splitX, rect.y, rect.width - splitX, rect.height)); | |
} | |
return true; | |
} | |
// 방을 생성하는 함수 (최소 및 최대 방 크기 내에서 랜덤한 크기의 방을 생성) | |
public void CreateRoom() | |
{ | |
int roomWidth = Random.Range(minRoomSize, Mathf.Min(maxRoomSize, rect.width)); | |
int roomHeight = Random.Range(minRoomSize, Mathf.Min(maxRoomSize, rect.height)); | |
int roomX = Random.Range(rect.x, rect.x + rect.width - roomWidth); | |
int roomY = Random.Range(rect.y, rect.y + rect.height - roomHeight); | |
room = new RectInt(roomX, roomY, roomWidth, roomHeight); | |
} | |
// 좌우 자식 노드에 방이 있을 경우, 두 방을 연결하는 통로를 생성 | |
public List<RectInt> ConnectRooms() | |
{ | |
if (left == null || right == null) | |
{ | |
return new List<RectInt>(); | |
} | |
// 왼쪽 방과 오른쪽 방의 중심을 기준으로 통로 연결 | |
Vector2Int leftCenter = new Vector2Int(left.room.x + left.room.width / 2, left.room.y + left.room.height / 2); | |
Vector2Int rightCenter = new Vector2Int(right.room.x + right.room.width / 2, right.room.y + right.room.height / 2); | |
List<RectInt> corridors = new List<RectInt>(); | |
// 가로 통로 생성 | |
if (leftCenter.x != rightCenter.x) | |
{ | |
RectInt corridor = new RectInt(Mathf.Min(leftCenter.x, rightCenter.x), leftCenter.y, Mathf.Abs(leftCenter.x - rightCenter.x), 1); | |
corridors.Add(corridor); | |
} | |
// 세로 통로 생성 | |
if (leftCenter.y != rightCenter.y) | |
{ | |
RectInt corridor = new RectInt(rightCenter.x, Mathf.Min(leftCenter.y, rightCenter.y), 1, Mathf.Abs(leftCenter.y - rightCenter.y)); | |
corridors.Add(corridor); | |
} | |
return corridors; | |
} | |
} | |
using UnityEngine; | |
using System.Collections.Generic; | |
public class BSPDungeonGenerator : MonoBehaviour | |
{ | |
public int width = 50; // 던전의 너비 | |
public int height = 50; // 던전의 높이 | |
public int maxDepth = 5; // 트리의 최대 깊이 | |
public GameObject roomPrefab; // 방을 나타내는 프리팹 | |
private BSPNode rootNode; | |
private List<RectInt> rooms; | |
private List<RectInt> corridors; | |
void Start() | |
{ | |
GenerateDungeon(); | |
} | |
void GenerateDungeon() | |
{ | |
rootNode = new BSPNode(new RectInt(0, 0, width, height)); | |
SplitTree(rootNode, 0); | |
rooms = new List<RectInt>(); | |
corridors = new List<RectInt>(); | |
CollectRoomsAndCorridors(rootNode); | |
DrawDungeon(); | |
} | |
// 트리를 재귀적으로 분할 | |
void SplitTree(BSPNode node, int depth) | |
{ | |
if (depth >= maxDepth || !node.Split()) | |
{ | |
node.CreateRoom(); // 분할할 수 없으면 방을 생성 | |
return; | |
} | |
SplitTree(node.left, depth + 1); | |
SplitTree(node.right, depth + 1); | |
} | |
// 트리에서 방과 통로를 수집 | |
void CollectRoomsAndCorridors(BSPNode node) | |
{ | |
if (node.left == null && node.right == null) | |
{ | |
rooms.Add(node.room); // 잎 노드(leaf)에 있는 방 수집 | |
} | |
else | |
{ | |
if (node.left != null) CollectRoomsAndCorridors(node.left); | |
if (node.right != null) CollectRoomsAndCorridors(node.right); | |
// 좌우 자식 노드를 연결하는 통로 수집 | |
corridors.AddRange(node.ConnectRooms()); | |
} | |
} | |
// 방과 통로를 시각적으로 표시 | |
void DrawDungeon() | |
{ | |
foreach (var room in rooms) | |
{ | |
Vector3 position = new Vector3(room.x + room.width / 2, room.y + room.height / 2, 0); | |
GameObject roomInstance = Instantiate(roomPrefab, position, Quaternion.identity); | |
roomInstance.transform.localScale = new Vector3(room.width, room.height, 1); | |
} | |
foreach (var corridor in corridors) | |
{ | |
Vector3 position = new Vector3(corridor.x + corridor.width / 2, corridor.y + corridor.height / 2, 0); | |
GameObject corridorInstance = Instantiate(roomPrefab, position, Quaternion.identity); | |
corridorInstance.transform.localScale = new Vector3(corridor.width, corridor.height, 1); | |
} | |
} | |
} | |
using System.Collections.Generic; | |
using UnityEngine; | |
public class SimpleBSPDepth : MonoBehaviour | |
{ | |
public int maxDepth = 3; // BSP 분할 최대 깊이 | |
public GameObject roomPrefab; // 방을 나타내는 프리팹 (크기 없음, 단순 시각화용) | |
private List<BSPNode> rooms; // 최종적으로 생성된 방 리스트 | |
void Start() | |
{ | |
// 루트 노드 생성 (크기 없이 분할 시작) | |
BSPNode rootNode = new BSPNode(); | |
rooms = new List<BSPNode>(); | |
// BSP 분할 시작 | |
SplitNode(rootNode, 0); | |
// 던전을 시각화 | |
DrawDungeon(); | |
} | |
// 노드를 분할하는 함수 (BSP의 분할 깊이에 따라 처리) | |
void SplitNode(BSPNode node, int depth) | |
{ | |
// 최대 분할 깊이에 도달했으면 방을 추가 | |
if (depth >= maxDepth) | |
{ | |
rooms.Add(node); // 분할하지 않고 방을 최종 노드로 추가 | |
return; | |
} | |
// 노드 분할 (좌우 또는 상하로 랜덤하게 분할) | |
node.Split(); | |
// 자식 노드를 재귀적으로 분할 | |
SplitNode(node.left, depth + 1); | |
SplitNode(node.right, depth + 1); | |
} | |
// 방을 시각화하는 함수 | |
void DrawDungeon() | |
{ | |
int offset = 5; // 방 간의 거리 오프셋 | |
foreach (BSPNode node in rooms) | |
{ | |
// 랜덤한 상하 또는 좌우 배치를 위한 좌표 설정 | |
Vector3 position = new Vector3(node.position.x * offset, node.position.y * offset, 0); | |
Instantiate(roomPrefab, position, Quaternion.identity); // 방을 임의 위치에 배치 | |
} | |
} | |
} | |
// BSP 노드 클래스 (크기 없이 분할만 처리) | |
public class BSPNode | |
{ | |
public BSPNode left; // 좌측 자식 노드 | |
public BSPNode right; // 우측 자식 노드 | |
public Vector2Int position; // 각 노드의 위치 (분할 시 이동 방향에 따른 좌표) | |
// 생성자 | |
public BSPNode() | |
{ | |
position = Vector2Int.zero; // 초기 위치를 (0, 0)으로 설정 | |
} | |
// 노드를 분할하는 함수 (크기 고려 없이 단순히 상하 또는 좌우로 분할) | |
public void Split() | |
{ | |
// 분할 방향 결정 (0: 좌우, 1: 상하) | |
bool splitHorizontally = Random.Range(0, 2) == 0; | |
// 좌우로 분할 | |
if (splitHorizontally) | |
{ | |
left = new BSPNode { position = new Vector2Int(position.x - 1, position.y) }; // 왼쪽으로 이동 | |
right = new BSPNode { position = new Vector2Int(position.x + 1, position.y) }; // 오른쪽으로 이동 | |
} | |
// 상하로 분할 | |
else | |
{ | |
left = new BSPNode { position = new Vector2Int(position.x, position.y + 1) }; // 위쪽으로 이동 | |
right = new BSPNode { position = new Vector2Int(position.x, position.y - 1) }; // 아래쪽으로 이동 | |
} | |
} | |
} | |
----- 밀도 기반 경로 탐색 알고리즘 (AI 관련) : 군중 제어, 군집 이동에 관한 알고리즘 ----- | |
충돌이나 혼잡을 피하기 위해 경로를 동적으로 조종하는 알고리즘 | |
using UnityEngine; | |
using UnityEngine.AI; | |
public class AgentDensityController : MonoBehaviour | |
{ | |
private NavMeshAgent agent; // NavMeshAgent 컴포넌트 | |
public Transform target; // 목표 지점 (Destination) | |
public float detectionRadius = 5.0f; // 밀도를 계산할 반경 | |
public LayerMask agentLayer; // 에이전트가 속한 레이어 (밀도 계산용) | |
private Vector3 originalDestination; // 에이전트의 최종 목표 지점 | |
private float densityCheckInterval = 0.5f; // 밀도 계산 주기 | |
private float timeSinceLastCheck = 0.0f; // 밀도 계산을 위한 시간 누적 값 | |
// 초기화 | |
void Start() | |
{ | |
agent = GetComponent<NavMeshAgent>(); // NavMeshAgent 컴포넌트 가져오기 | |
originalDestination = target.position; // 목표 지점 설정 | |
agent.SetDestination(originalDestination); // 목표로 이동 시작 | |
} | |
// 매 프레임마다 밀도 계산 및 경로 조정 | |
void Update() | |
{ | |
timeSinceLastCheck += Time.deltaTime; | |
if (timeSinceLastCheck >= densityCheckInterval) | |
{ | |
float density = CalculateDensity(); // 밀도 계산 | |
AdjustMovementBasedOnDensity(density); // 밀도에 따라 경로 조정 | |
timeSinceLastCheck = 0.0f; // 밀도 체크 시간 초기화 | |
} | |
} | |
// 주변 에이전트 밀도를 계산하는 함수 | |
float CalculateDensity() | |
{ | |
Collider[] agentsNearby = Physics.OverlapSphere(transform.position, detectionRadius, agentLayer); | |
return agentsNearby.Length; // 주변 에이전트 수를 밀도로 간주 | |
} | |
// 밀도에 따라 경로를 재조정하는 함수 | |
void AdjustMovementBasedOnDensity(float density) | |
{ | |
if (density > 5) // 밀도가 높으면 경로 회피 | |
{ | |
Vector3 avoidanceDirection = GetAvoidanceDirection(); // 새로운 회피 경로 계산 | |
agent.SetDestination(avoidanceDirection); // 회피 경로로 이동 | |
} | |
else // 밀도가 낮으면 원래 목표 지점으로 이동 | |
{ | |
agent.SetDestination(originalDestination); | |
} | |
} | |
// 밀도가 높은 구역을 피할 회피 경로를 계산하는 함수 | |
Vector3 GetAvoidanceDirection() | |
{ | |
Vector3 randomDirection = Random.insideUnitSphere * 10.0f; // 무작위 방향으로 회피 | |
randomDirection += transform.position; // 현재 위치에서 회피 방향을 계산 | |
NavMeshHit hit; // NavMesh 내에서 유효한 위치 탐색 | |
NavMesh.SamplePosition(randomDirection, out hit, 10.0f, NavMesh.AllAreas); | |
return hit.position; // 유효한 회피 경로 반환 | |
} | |
} | |
// 다수의 에이전트를 생성하여 밀도 기반 경로 탐색을 테스트하는 스크립트 | |
public class AgentSpawner : MonoBehaviour | |
{ | |
public GameObject agentPrefab; // 에이전트 프리팹 | |
public int agentCount = 50; // 생성할 에이전트 수 | |
// Start에서 에이전트 생성 | |
void Start() | |
{ | |
for (int i = 0; i < agentCount; i++) | |
{ | |
// 임의 위치에 에이전트를 생성 | |
Vector3 spawnPosition = new Vector3(Random.Range(-10, 10), 0, Random.Range(-10, 10)); | |
Instantiate(agentPrefab, spawnPosition, Quaternion.identity); | |
} | |
} | |
} | |
----- Boid 알고리즘 (AI 관련) : 군중 제어, 군집 이동에 관한 알고리즘 ----- | |
새, 물고기, 군중 충돌하지 않고 자연스럽게 군집해서 이동할때 사용하는 알고리즘 | |
가중치와 경계처리 포함해서 각각 (정렬, 응집, 분리) 방향 벡터를 구해서 이동 부분에 넣어주면 됨. | |
using UnityEngine; | |
using System.Collections.Generic; | |
public class Boid : MonoBehaviour | |
{ | |
public float speed = 5.0f; | |
public float neighborRadius = 3.0f; | |
public float separationDistance = 1.0f; | |
public Vector3 bounds = new Vector3(50, 50, 50); // 시뮬레이션 영역 | |
private Vector3 velocity; | |
// 각 에이전트의 행동을 계산하는 함수 | |
void Update() | |
{ | |
List<Boid> neighbors = GetNeighbors(); | |
Vector3 alignment = Align(neighbors) * alignmentWeight; | |
Vector3 cohesion = Cohere(neighbors) * cohesionWeight; | |
Vector3 separation = Separate(neighbors) * separationWeight; | |
Vector3 boundaryForce = CheckBounds(); | |
Vector3 moveDirection = alignment + cohesion + separation + boundaryForce; | |
velocity = Vector3.Lerp(velocity, moveDirection, Time.deltaTime); | |
transform.position += velocity * Time.deltaTime * speed; | |
transform.forward = velocity.normalized; | |
} | |
// 근처의 보이드 리스트를 가져오는 함수 | |
List<Boid> GetNeighbors() | |
{ | |
Collider[] colliders = Physics.OverlapSphere(transform.position, neighborRadius); | |
List<Boid> neighbors = new List<Boid>(); | |
foreach (Collider collider in colliders) | |
{ | |
Boid boid = collider.GetComponent<Boid>(); | |
if (boid != null && boid != this) | |
{ | |
neighbors.Add(boid); | |
} | |
} | |
return neighbors; | |
} | |
// 정렬(Alignment) 규칙 | |
Vector3 Align(List<Boid> neighbors) | |
{ | |
Vector3 avgDirection = Vector3.zero; | |
if (neighbors.Count == 0) return avgDirection; | |
foreach (Boid neighbor in neighbors) | |
{ | |
avgDirection += neighbor.velocity; | |
} | |
avgDirection /= neighbors.Count; | |
return avgDirection.normalized; // 평균 방향으로 정렬 | |
} | |
// 응집(Cohesion) 규칙 | |
Vector3 Cohere(List<Boid> neighbors) | |
{ | |
Vector3 centerOfMass = Vector3.zero; | |
if (neighbors.Count == 0) return centerOfMass; | |
foreach (Boid neighbor in neighbors) | |
{ | |
centerOfMass += neighbor.transform.position; | |
} | |
centerOfMass /= neighbors.Count; | |
return (centerOfMass - transform.position).normalized; // 중심으로 이동 | |
} | |
// 분리(Separation) 규칙 | |
Vector3 Separate(List<Boid> neighbors) | |
{ | |
Vector3 separationForce = Vector3.zero; | |
foreach (Boid neighbor in neighbors) | |
{ | |
float distance = Vector3.Distance(transform.position, neighbor.transform.position); | |
if (distance < separationDistance) | |
{ | |
separationForce += (transform.position - neighbor.transform.position) / distance; | |
} | |
} | |
return separationForce.normalized; // 가까운 보이드로부터 멀어지기 | |
} | |
// 시뮬레이션 경계를 벗어날 때 중심으로 이동시키는 힘 | |
Vector3 CheckBounds() | |
{ | |
Vector3 boundaryForce = Vector3.zero; | |
if (transform.position.x > bounds.x || transform.position.x < -bounds.x) | |
boundaryForce.x = -transform.position.x; | |
if (transform.position.y > bounds.y || transform.position.y < -bounds.y) | |
boundaryForce.y = -transform.position.y; | |
if (transform.position.z > bounds.z || transform.position.z < -bounds.z) | |
boundaryForce.z = -transform.position.z; | |
return boundaryForce.normalized; | |
} | |
} | |
----- float Mathf.PerlinNoise(float x, float y); ------ | |
절차적 맵 생성, 카메라 쉐이킹 (Camera Shake)에 사용할 수 있는 기본적인 랜덤 함수. | |
결과값은 0에서 1 사이의 값이다. | |
핵심은 0- 1 값이 2차원적으로 랜덤하게 이어져서 나온다는 점. 응용은 자율. | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
.gitignore 파일 공유
.editorconfig 파일 공유