public override void OnConnected(EndPoint endPoint)
{
PlayerInfoReq packet = new PlayerInfoReq() { packetId = (ushort)PacketID.PlayerInfoReq, playerId = 1001 };
// Send
ArraySegment<byte> s = SendBufferHelpher.Open(4096);
bool success = true;
ushort count = 0;
count += 2;
success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset + count, s.Count - count), packet.packetId);
count += 2;
success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset + count, s.Count - count), packet.playerId);
count += 8;
success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset, s.Count), count);
ArraySegment<byte> sendBuff = SendBufferHelpher.Close(count);
if (success)
{
Send(sendBuff);
}
}
public override void OnRecvPacket(ArraySegment<byte> buffer)
{
ushort count = 0;
ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
count += 2;
ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count);
count += 2;
switch ((PacketID)id)
{
case PacketID.PlayerInfoReq:
{
long playerId = BitConverter.ToInt64(buffer.Array, buffer.Offset + count);
count += 8;
Console.WriteLine($"PlayerInfoReq {playerId}");
}
break;
}
Console.WriteLine($"RecvPacket ID: {id}, Size {size}");
}
입력받은 키 바인딩해서 액션 실행하기
public class TopDownController : MonoBehaviour
{
public event Action<Vector2> OnMoveEvent;
public event Action<Vector2> OnLookEvent;
public void CallMoveEvent(Vector2 direction)
{
OnMoveEvent?.Invoke(direction);
}
public void CallLookEvent(Vector2 direction)
{
OnLookEvent?.Invoke(direction);
}
}
public class PlayerInputController : TopDownCharacterController
{
private Camera _camera;
private void Awake()
{
_camera = Camera.main;
}
public void OnMove(InputValue value)
{
// Debug.Log("OnMove" + value.ToString());
Vector2 moveInput = value.Get<Vector2>().normalized;
CallMoveEvent(moveInput);
}
public void OnLook(InputValue value)
{
// Debug.Log("OnLook" + value.ToString());
Vector2 newAim = value.Get<Vector2>();
Vector2 worldPos = _camera.ScreenToWorldPoint(newAim);
newAim = (worldPos - (Vector2)transform.position).normalized;
if (newAim.magnitude >= .9f)
// Vector 값을 실수로 변환
{
CallLookEvent(newAim);
}
}
public void OnFire(InputValue value)
{
Debug.Log("OnFire" + value.ToString());
}
}
데이터를 따로 빼놔서 에셋으로 저장하기
언리얼 Data Table 하고 비슷하다
[CreateAssetMenu(fileName = "DefaultAttackSO", menuName = "TopDownController/Attacks/Default", order = 0)]
public class AttackSO : ScriptableObject
{
[Header("Attack Info")]
public float size;
public float delay;
public float power;
public float speed;
public LayerMask target;
[Header("Knock Back Info")]
public bool isOnKnockBack;
public float knockBackPower;
public float knockBackTime;
}
PlayerRangeAttack SO에서 데이터 넣을 때 Projectile Color에 알파가 0으로 들어가 있었다.
화살 생성하는 부분에 브레이크 걸고 씬에서 화살 확인했다. 프리팹에서는 제대로 하얀색인데 실행하면 알파가 0이 된다.
Player에 PlayerInput컴포넌트가 있다
PlayerInput Actions에다가 만들어놓은 InputAction을 연결한다
OnMove, OnLook, OnFire을 호출한다
// PlayerInputController
public void OnFire(InputValue value)
{
IsAttacking = value.isPressed;
}
// TopDownController
public event Action<AttackSO> OnAttackEvent;
private void Update()
{
HandleAttackDelay();
}
private void HandleAttackDelay()
{
if(timeSinceLastAttack < stats.CurrentStat.attackSO.delay)
{
timeSinceLastAttack += Time.deltaTime;
}
else if (IsAttacking && timeSinceLastAttack > stats.CurrentStat.attackSO.delay)
{
timeSinceLastAttack = 0f;
CallAttackEvent(stats.CurrentStat.attackSO);
}
}
private void CallAttackEvent(AttackSO attackSO)
{
OnAttackEvent?.Invoke(stats.CurrentStat.attackSO);
}
// TopDownShooting
private void Start()
{
controller.OnAttackEvent += OnShoot;
controller.OnLookEvent += OnAim;
}
private void OnShoot(AttackSO attackSO)
{
RangedAttackSO rangedAttackSO = attackSO as RangedAttackSO;
if (rangedAttackSO == null) return;
float projectileAngleSpace = rangedAttackSO.multipleProjectilesAngle;
int numberOfProjectilesPerShot = rangedAttackSO.numberOfProjectilesPerShot;
float minAngle = - (numberOfProjectilesPerShot / 2f) * projectileAngleSpace * 0.5f * rangedAttackSO.multipleProjectilesAngle;
for(int i = 0; i < numberOfProjectilesPerShot; i++)
{
float angle = minAngle + i * projectileAngleSpace;
float randomSpread = Random.Range(-rangedAttackSO.spread, rangedAttackSO.spread);
angle += randomSpread;
CreateProjectile(rangedAttackSO, angle);
}
}
private void CreateProjectile(RangedAttackSO rangedAttackSO, float angle)
{
GameObject obj = Instantiate(projectile);
obj.transform.position = projectileSpawnPosition.position;
ProjectileController attackController = obj.GetComponent<ProjectileController>();
attackController.InitalizeAttack(RotateVector2(aimDirection, angle), rangedAttackSO);
}
// ProjectileController
public void InitalizeAttack(Vector2 direction, RangedAttackSO attackData)
{
this.direction = direction;
this.attackData = attackData;
isReady = true;
UpdateProjectileSprite();
trailRenderer.Clear();
currentDuration = 0;
spriteRenderer.color = attackData.projecileColor;
transform.right = this.direction;
}
Animator 컴포넌트가 MainSprite가 아닌 Goblin에 있었다. Animation 에셋을 만들 때 Goblin에다가 넣고 만들었나보다. 계층구조 상에서 상대적인 위치가 잘못되어 있었다.
계속 곱해지는 부분에서 이상하다. 값이 누적해서 곱해지고 있다.
Vector2가 Struct 였다.
처음 만들었던 버전에서 ApplyMovement를 호출해서 인자로 movementDirection을 넘긴다. 그리고 speed랑 곱해서 velocity를 설정해 준다. 두번째 버전으로 함수 호출 안하고 FixedUpdate 부분에서 바로 코드 넣었다. movementDirection 은 멤버변수고 direction은 지역 변수다.
Vector2가 class인 줄 알고 코드는 문제 없다고 생각했다. 왜냐하면 new로 호출하기 때문에 너무 당연히 힙에 만들어지고 함수호출할 때 힙주소 넘길 줄 알았다. 그런데 struct였다. 그래서 1에서 direction은 스택에 값이 새로 만들어진 거여서 변경해도 movementDirection에 영향을 주고있지 않았다.
기존 코드가 잘 작동하니까 잘 써놨겠지~ 하면서 복붙하고 이상한 점을 늦게 알아차렸다.
private Vector2 movementDirection = Vector2.zero;
private void FixedUpdate()
{
ApplyMovement(movementDirection);
}
// 1
private void ApplyMovement(Vector2 direction)
{
direction = direction * speed;
movementRigidbody.velocity = direction;
Debug.Log(direction);
}
// 2
private void ApplyMovement(Vector2 direction)
{
movementDirection = movementDirection * speed;
_rigidbody2D.velocity = movementDirection;
Debug.Log(movementDirection);
}
Player - FixedUpdate 에서 velocity를 이용해 움직인다.
Camera - LateUpdate에서 player의 위치에 따라 Lerp로 부드럽게 움직인다.
업데이트 프레임이 안맞는다 → 플레이어가 이상하게 움직인다.
해결방법
1 Lerp를 뺀다. 카메라 부드럽게 안움직인다.
2 Camera를 FixedUpdate로 바꿔서 맞춘다 - 카메라 Lerp 움직임 가능
3 Camera - LateUpdate에서 플레이어 위치에 따라 이동 / Player - FixedUpdate에서 위치 이동, RigidbodyInterpolation 값 None -> Interpolate 으로 선택
3번이 맞는 해결 방법 같다. 왜냐하면 카메라 이동은 모든 오브젝트의 Update가 끝난 후에 찍어야 한다. 그래서 LateUpdate에서 마지막에 카메라 이동처리를 해준다.
https://docs.unity3d.com/ScriptReference/MonoBehaviour.LateUpdate.html
보간은 물리 타임스텝 업데이트 사이에 해당하는 프레임에서 리지드바디의 포즈를 계산하여 눈에 보이는 지터를 줄입니다. 플레이어 캐릭터 게임 오브젝트 및 카메라가 따라가는 다른 게임 오브젝트에 특히 유용합니다. 기본적으로 보간은 비활성화되어 있습니다.
https://docs.unity3d.com/ScriptReference/RigidbodyInterpolation.html
해본 것
유니티 에셋에서 Reimport All 하기 X
폴더에서 Library, obj, Temp 지우기 X
.csproj지우기, Refresh를 눌러서 References 다시 찾기 O
Animator Override Controller로 구현해서 개선할 수 있다.
BlentTree 언리얼에서 블렌드 스페이스 애니메이션과 비슷하게 기능이 확장되어 있는 것 같다.
protected void Bind<T>(Type type) where T : UnityEngine.GameObject
GameObject는 sealed 되어있어서 상속해서 못 쓴다. 그래서 제너릭 필요가 없다.
public sealed class GameObject : Object
UI들은 GameObject가 아니라 Object에서 상속받았다.
UnityEngine.UI.~~, UnityEngine.GameObject, TMPro.TextMeshProUGUI 상위에 Object가 있다.
// UI_NamePopup
enum GameObjects
{
InputField
}
public override bool Init()
{
if (base.Init() == false)
{
return false;
}
BindObject(typeof(GameObjects));
return true;
}
// UI_Base
protected void BindObject(Type type) { Bind<GameObject>(type); }
protected void Bind<T>(Type type) where T : UnityEngine.Object
{
string[] names = Enum.GetNames(type);
}
Type이 IReflect 기능을 갖고있다
public abstract class Type : MemberInfo, IReflect {}
public interface IReflect {}
한글 폰트 받아서 에셋 생성 해준다. Update Atlas Texture 눌러서 CharacterSet을 Custoum Range로 바꾸고 한글이 지원하는 범위에 맞게 누른다. 그래서 그 생성된 에셋 폰트를 쓴다.
@UI_Root 만들고 거기 밑에 다 넣어버린다 프리팹으로 만들어 놓은 UI 부른다.
EventSystem이 없었다. Input 처리, Raycasting, 이벤트 보내주는 역할을 한다.
https://docs.unity3d.com/2018.2/Documentation/ScriptReference/EventSystems.EventSystem.html
private void Start()
{
GameObject prefab = Resources.Load<GameObject>($"Prefabs/UI/UI_OptionPopup");
if (prefab == null)
{
Debug.Log($"Failed to load prefab : UI/UI_OptionPopup");
}
UI_OptionPopup optionPopup = Instantiate(prefab) as UI_OptionPopup;
}
Instantiate 가 반환하는 것은 GameObject이다. UI_OptionPopup은 컴포넌트이다. 그래서 형변환이 불가능하다.
warning CS0108: 'PlayerInputController.camera' hides inherited member 'Component.camera'. Use the new keyword if hiding was intended.
public class PlayerInputController : MonoBehaviour
{
private Camera camera;
}
camera → _camera로 바꾼다. 이미 Component.camera가 있기 때문에 그렇다.
/// <summary>
/// Sound를 BGM, Effect로 나누어 관리
/// Effect 오디오클립만 _audioClips Dictionary에 저장, 빠르게 꺼내쓰기
/// </summary>
public class SoundManager
{
private AudioSource[] _audioSources = new AudioSource[(int)Define.Sound.Max];
private Dictionary<string, AudioClip> _audioClips = new Dictionary<string, AudioClip>();
private GameObject _soundRoot = null;
public void Init()
{
if (_soundRoot == null)
{
_soundRoot = GameObject.Find("@SoundRoot");
if (_soundRoot == null)
{
_soundRoot = new GameObject { name = "@SoundRoot" };
UnityEngine.Object.DontDestroyOnLoad(_soundRoot);
string[] soundTypeNames = System.Enum.GetNames(typeof(Define.Sound));
for (int count = 0; count < soundTypeNames.Length - 1; count++)
{
GameObject go = new GameObject { name = soundTypeNames[count] };
_audioSources[count] = go.AddComponent<AudioSource>();
go.transform.parent = _soundRoot.transform;
}
_audioSources[(int)Define.Sound.Bgm].loop = true;
}
}
}
public void Play(Define.Sound type, string path)
{
AudioSource audioSource = _audioSources[(int)type];
// Sound/ 경로 추가
if (path.Contains("Sound/") == false)
{
path = string.Format("Sound/{0}", path);
}
// BGM 재생
if (type == Define.Sound.Bgm)
{
AudioClip audioClip = Resources.Load<AudioClip>(path);
if (audioClip == null)
{
Debug.LogError($"AudioClip Missing : {path}");
return;
}
if (audioSource.isPlaying)
{
audioSource.Stop();
}
audioSource.clip = audioClip;
audioSource.Play();
}
// Effect 재생
else if (type == Define.Sound.Effect)
{
AudioClip audioClip = GetAudioClip(path);
if (audioClip == null)
{
return;
}
audioSource.PlayOneShot(audioClip);
return;
}
}
private AudioClip GetAudioClip(string path)
{
AudioClip audioClip = null;
// Dictionary에 저장된 AudioClip이 있다면 반환
if (_audioClips.TryGetValue(path, out audioClip))
{
return audioClip;
}
// Resources.Load로 AudioClip을 찾아서 Dictionary에 저장
audioClip = Resources.Load<AudioClip>(path);
if (audioClip == null)
{
Debug.LogError($"AudioClip Missing : {path}");
return null;
}
_audioClips.Add(path, audioClip);
return audioClip;
}
}
Asset / Package / ProjectSetting 만 있으면 된다.
Library / Logs / Temp / UserSettings 지우기
public static T GetOrAddComponent<T>(this UnityObject uo) where T : Component
{
return uo.GetComponent<T>() ?? uo.AddComponent<T>();
}
Extension Method로 기능을 추가한다. GameObject가 Sealed 클래스로 상속이 불가능하다. 그래서 ComponentHolderProtocol에서 기능을 추가한 것 같다. 여기는 열려있어서 내용을 볼 수 있었다.
public static T GetOrAddComponent<T>(this UnityObject uo) where T : Component
{
T component = uo.GetComponent<T>();
if (component == null)
{
component = uo.AddComponent<T>();
}
return component;
}
이 내용과 위에 내용이 같다.
Sprite[] sprites = Resources.LoadAll<Sprite>("Sprites/GameIcon");
// 스프라이트 배열에서 원하는 스프라이트 찾기
foreach (Sprite sprite in sprites)
{
if (sprite.name == "GameIcon_16")
{
SoundOff = sprite;
}
else if (sprite.name == "GameIcon_5")
{
SoundOn = sprite;
}
}
전체를 불러와서 잘라서 써야 한다.
private void GetLeftTimeText()
{
float leftSecond = Managers.Game.LeftSecond;
TimeSpan timeSpan = TimeSpan.FromSeconds(leftSecond);
LeftTimeText.text = $"{timeSpan.Minutes:D2}:{timeSpan.Seconds:D2}";
}
private void GetScoreText()
{
ScoreText.text = $"{Managers.Game.Score:N0}점";
}
public class ObjectPool : MonoBehaviour
{
// 오브젝트 풀 데이터를 정의할 데이터 모음 정의
[System.Serializable]
public class Pool
{
public string tag;
public GameObject prefab;
public int size;
}
public List<Pool> Pools;
public Dictionary<string, List<GameObject>> poolDictionary;
private void Awake()
{
poolDictionary = new Dictionary<string, List<GameObject>>();
foreach (var pool in Pools)
{
List<GameObject> objectPool = new List<GameObject>();
for (int i = 0; i < pool.size; i++)
{
GameObject obj = Instantiate(pool.prefab);
obj.SetActive(false);
objectPool.Add(obj);
}
poolDictionary.Add(pool.tag, objectPool);
}
}
public GameObject SpawnFromPool(string tag)
{
if (!poolDictionary.ContainsKey(tag))
return null;
foreach (GameObject obj in poolDictionary[tag])
{
if (!obj.activeInHierarchy)
{
obj.SetActive(true);
return obj;
}
}
return null;
}
}
// 1 List
foreach (var pool in Pools)
{
List<GameObject> objectPool = new List<GameObject>();
for (int i = 0; i < pool.size; i++)
{
GameObject obj = Instantiate(pool.prefab);
obj.SetActive(false);
objectPool.Add(obj);
}
poolDictionary.Add(pool.tag, objectPool);
}
// 2 Dictionary
foreach (GameObject obj in poolDictionary[tag])
{
if (!obj.activeInHierarchy)
{
obj.SetActive(true);
return obj;
}
}
1에서는 ObjectPool GameObject를 Awake()했을 때 poolDictionary에다가 Pools에 있는 내용대로 Instantiate을 해서 메모리 할당을 해주는 내용이다. objectPoold은 리스트로 동일한 타입을 저장해 놓은 동적 배열이다. 메모리에 연속해서 붙어있다. 특징은 동적으로 메모리가 증가해 Add를 했는데 공간이 없으면 다시 필요한 메모리를 요청해서 할당받는다.
2는 poolDictionary에다가 만들어 놓은 객체를 사용할 때 쓴다. Dictionary의 특징은 해시기반으로 만들어져서 빠르게 접근할 수 있는 것이다. <string, ~ > 키를 가지고 O(1)에 찾을 수 있다. 그래서 여기서는 원하는 GameObject의 타입을 string으로 찾아서 list를 들고온다. 그리고 거기서 돌면서 안쓰고 있는 오브젝트를 찾아서 켜서 반환해준다.
List와 Dictionary 모두 O(1)에 데이터에 접근할 수 있다. 하지만 데이터가 추가될 때 방식, 데이터를 접근할 때 인덱스, 해시를 이용하는 것이 차이점이다.
Instantiate Destroy를 많이 하면 안된다. 왜냐하면 메모리를 할당 해제하면 그것 자체로도 OS에 요청해서 systemcall을 한다. 그리고 GC가 안쓰는 메모리 해제해주는데 그것도 자주 발생하면 게임이 끊긴다. 그리고 필요할 때 할당해제하면 메모리도 파편화가 된다. 그래서 한번에 커다란 덩어리로 만들어 놓아서 메모리에 미리 올려놓는 것이 자주 사용하는 오브젝트는 성능 향상에 좋다.
Animator.String(“isRun”)을 사용해서 참조한다. 애니메이션이 설정해놓은 Transition에 맞게 변한다.public class TD : TDP
{
private static readonly int IsRun = Animator.String("isRun");
protected override void Awake()
{
base.Awake();
}
private void Start()
{
characterController.MovingEvent += Run;
}
private void Run(Vector2 vector)
{
animator.SetBool(IsRun, vector.magnitude > .5f);
}
}
일주일동한 진행한 유니티 팀플이 끝났다
https://github.com/SandyLee-00/Unity_Dodge
Good : 화면공유를 하고 트러블 슈팅을 하면서 문제점을 잘 찾을 수 있었다. 기능구현이 생각보다 빠르게 되었다.
Bad : 깃으로 협업을 하면서 프리팹, 씬, 컴포넌트 변수들이 제대로 안들어가서 다시 해야하는게 반복되었다. 코드 페이지 인코딩 부분을 초반에 신경쓰지 않아서 그냥 진행했고 한글주석이 계속해서 깨졌다.
느낀점 : 소통이 잘 되어서 문제가 생겼을 때 해결이 빠르게 되었다. 디버깅 시간을 고려해서 일정을 짜야겠다.
TODO : UI, Resource Manager 만들어서 좀 더 성능과 재사용성을 높일 수 있었는데 리팩토링할 시간이 없었다.
The layout "C:/Users/syjy8/AppData/Roaming/Unity/Editor-5.x/Preferences\Layouts\current\default-2022.dwlt" could not be fully loaded, this can happen when the layout contains EditorWindows not available in this project.
UnityEditor.WindowLayout:LoadDefaultWindowPreferences ()

Clean Cache를 누른다
static 멤버이다. 클래스가 처음 로드 될 때 메모리에 공간이 할당된다. 하지만 실제 인스턴스의 값이 할당되지 않는다.instance 가 null인지 확인한다. FindObjectOfType : 현재 씬에서 T 타입 객체가 있는지 찾는다. 있으면 그 싱글턴 컴포넌트를 instance에 할당한다.new GameObject() : 씬에서 그 타입 컴포넌트가 없으면 붙여질 singletonObject 를 만들고 거기에 싱글톤 컴포넌트를 붙힌다.Awake : MonoBehaviour을 상속받아서 라이프 사이클대로 돌아간다. 컴포넌트가 붙어있는 게임 오브젝트, 그 밑에 컴포넌트가 씬이 전환될 때 안 사라지게 한다.instance 가 static 변수여서 클래스의 모든 객체가 동일한 객체를 참조한다. → 싱글턴public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T instance;
public static T Instance
{
get
{
if (instance == null)
{
instance = (T)FindObjectOfType(typeof(T));
if (instance == null)
{
GameObject singletonObject = new GameObject();
instance = singletonObject.AddComponent<T>();
}
}
return _instance;
}
}
public void Awake()
{
DontDestroyOnLoad(instance);
}
}
public string _path = Application.persistentDataPath + "/SaveData.json";
public void SaveGame()
{
string jsonStr = JsonUtility.ToJson(Managers.Game.SaveData);
File.WriteAllText(_path, jsonStr);
Debug.Log($"Save Game Completed : {_path}");
}
public bool LoadGame()
{
if (File.Exists(_path) == false)
return false;
string fileStr = File.ReadAllText(_path);
GameData data = JsonUtility.FromJson<GameData>(fileStr);
if (data != null)
{
Managers.Game.SaveData = data;
}
Debug.Log($"Save Game Loaded : {_path}");
return true;
}
ArrayOfTextData로 직렬화/역직렬화 된다.TextData 요소에 매핑된다.[Serializable, XmlRoot("ArrayOfTextData")]
public class TextDataLoader : ILoader<int, TextData>
{
[XmlElement("TextData")]
public List<TextData> _textData = new List<TextData>();
public Dictionary<int, TextData> MakeDictionary()
{
Dictionary<int, TextData> dictionary = new Dictionary<int, TextData>();
foreach(TextData textData in _textData)
{
dictionary.Add(textData.ID, textData);
}
return dictionary;
}
}
public static T Instantiate<T>(T original, Transform parent) where T : Object
{
return Instantiate(original, parent, worldPositionStays: false);
}
new()가 왜 있지?
new() 제약 조건이 필요하다. 컴파일러는 해당 제네릭 타입이 매개변수가 없는 기본 생성자를 가지고 있는 것을 보장한다.using이 왜 붙어있지?
Dispose 메서드를 호출하여 리소스를 해제한다. MemoryStream 객체가 더 이상 필요하지 않게 되었을 때 자동으로 정리해줄 수 있다.private Loader LoadXml<Loader, Key, Item>(string name) where Loader : ILoader<Key, Item>, new()
{
XmlSerializer serializer = new XmlSerializer(typeof(Loader));
TextAsset textAsset = Resources.Load<TextAsset>($"Data/{name}");
if (textAsset == null)
{
Debug.LogError($"DataManager::LoadXml() failed. name={name}");
return default;
}
using (MemoryStream stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(textAsset.text)))
{
return (Loader)serializer.Deserialize(stream);
}
}
public static void BindUIEvent(this GameObject _gameObject, Action action, Define.UIEvent type = Define.UIEvent.Click)
{
UI_EventHandler ui_EventHandler = _gameObject.GetOrAddComponent<UI_EventHandler>();
switch (type)
{
case Define.UIEvent.Click:
ui_EventHandler.OnClickHandler -= action;
ui_EventHandler.OnClickHandler += action;
break;
case Define.UIEvent.Pressed:
ui_EventHandler.OnPressedHandler -= action;
ui_EventHandler.OnPressedHandler += action;
break;
case Define.UIEvent.PointerDown:
ui_EventHandler.OnPointerDownHandler -= action;
ui_EventHandler.OnPointerDownHandler += action;
break;
case Define.UIEvent.PointerUp:
ui_EventHandler.OnPointerUpHandler -= action;
ui_EventHandler.OnPointerUpHandler += action;
break;
}
}
popup은 Destory 한 다음에 여기서 안쓰인다. 그리고 Destroy로 해당 GameObject 지우면 플레그 설정하고 프레임 끝날 때 한번에 지운다. 붙어있던 Component도 같이 사라져서 UI_Popup 타입으로 붙어있던 popup도 사라진다. 굳이 필요할까?
public void ClosePopupUI(UI_Popup popup)
{
if (popupStack.Count == 0)
{
Debug.LogError("UIManager::ClosePopupUI() popupStack is empty.");
return;
}
if(popupStack.Peek() != popup)
{
Debug.LogError("UIManager::ClosePopupUI() popupStack.Peek() != popup.");
return;
}
GameObject.Destroy(popupStack.Pop().gameObject);
popup = null;
order--;
}
Text가 아니라 타입이 TextMeshProUGUI이다.
protected void BindText(Type type) { Bind<TextMeshProUGUI>(type); }
이거 사용하는걸로 바꿨다.
PlayerInputController의 OnMove / OnLook / OnJump Debug.log를 해도 호출이 안된다.
DontDestroyOnLoad는 게임 오브젝트 단위로 적용되므로, instance가 부착된 게임 오브젝트와 그 자식 및 모든 부착된 컴포넌트가 씬 전환 시에도 파괴되지 않고 그대로 유지한다.public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T instance;
public static T Instance
{
get
{
if (instance == null)
{
instance = (T)FindObjectOfType(typeof(T));
if (instance == null)
{
GameObject singletonObject = new GameObject();
instance = singletonObject.AddComponent<T>();
}
}
return _instance;
}
}
public void Awake()
{
DontDestroyOnLoad(instance);
}
}