Skip to content

Instantly share code, notes, and snippets.

@Curookie
Last active July 30, 2025 04:41
Show Gist options
  • Save Curookie/42c979a7de7d656ec8cf6b8c01ea0457 to your computer and use it in GitHub Desktop.
Save Curookie/42c979a7de7d656ec8cf6b8c01ea0457 to your computer and use it in GitHub Desktop.
Unity 유니티 실무2
유니티 실무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차원적으로 랜덤하게 이어져서 나온다는 점. 응용은 자율.
@Curookie
Copy link
Author

Curookie commented May 1, 2025

.gitignore 파일 공유

# This .gitignore file should be placed at the root of your Unity project directory
#
# Get latest from https://github.com/github/gitignore/blob/main/Unity.gitignore
#
/[Ll]ibrary/
/[Tt]emp/
/[Oo]bj/
/[Bb]uild/
/[Bb]uilds/
/[Ll]ogs/
/[Uu]ser[Ss]ettings/
/[Mm]emoryCaptures/
/[Rr]ecordings/

# Asset meta data should only be ignored when the corresponding asset is also ignored
!/[Aa]ssets/**/*.meta

# Uncomment this line if you wish to ignore the asset store tools plugin
/[Aa]ssets/AssetStoreTools*

# Autogenerated Jetbrains Rider plugin
/[Aa]ssets/Plugins/Editor/JetBrains*

# Visual Studio Code cache directory
.vscode/

# Visual Studio cache directory
.vs/

# Gradle cache directory
.gradle/

# Autogenerated VS/MD/Consulo solution and project files
ExportedObj/
.consulo/
*.csproj
*.unityproj
*.sln
*.suo
*.tmp
*.user
*.userprefs
*.pidb
*.booproj
*.svd
*.opendb
*.VC.db

# Ignore binaries
*.mdb
*.pdb
*.exe
*.so
*.dylib

# But keep their .meta files
!*.mdb.meta
!*.pdb.meta
!*.exe.meta
!*.so.meta
!*.dylib.meta

# Unity3D generated file on crash reports
sysinfo.txt

# Builds
*.apk
*.aab
*.unitypackage
*.app

# Crashlytics generated file
crashlytics-build.properties

# Packed Addressables
/[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin*

# Temporary auto-generated Android Assets
/[Aa]ssets/[Ss]treamingAssets/build_info*


# ---- CUSTOM ---- *[  MUST CHANGE SMART MERGE's UNTIY VERSION ex) 2022.3.34f1  ]

# Smart Merge
[merge]
    tool = unityyamlmerge

[mergetool "unityyamlmerge"]
    trustExitCode = false
    cmd = 'C:\\Program Files\\Unity\\Hub\\Editor\\2022.3.34f1\\Editor\\Data\\Tools\\UnityYAMLMerge.exe' merge -p "$BASE" "$REMOTE" "$LOCAL" "$MERGED"

# MacOS Setting
.DS_Store

# ETC
*.keystore
/Build_BurstDebugInformation_DoNotShip/

.editorconfig 파일 공유

root = true

[*.cs]
csharp_new_line_before_open_brace = namespace
csharp_new_line_before_else = false
csharp_new_line_before_catch = false
csharp_new_line_before_finally = false

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment