목차
Design Pattern
25.01.03
Singleton
: 클래스가 자신의 인스턴스 하나만 인스턴스화 할 수 있도록 보장해준다. --> 해당 단일 인스턴스에 대한 손쉬운 전역 엑세스를 제공한다.
>> 하나의 객체만 존재할 필요가 있을 때 사용된다. ex) 상태창, 게임 매니저, 오디오 매니저, 파일 관리자, UI Setting 등등
>> Singleton.cs
using UnityEngine;
// T는 MonoBehaviour를 상속하는 형식만 가능
public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T instance;
public static T Instance
{
get
{
// 강사님은 이거 생략하신다.
if (instance == null)
{
instance = FindObjectOfType(typeof(T)) as T; // 현재 Scene에 존재하는 T 타입 객체를 검색
}
if (instance == null) // 검색했음에도 null
{
GameObject obj = new GameObject(); // 새 GameObject 생성
obj.name = typeof(T).Name; // GameObject 이름을 T의 이름으로 설정
obj.AddComponent<T>(); // T 타입의 Component 추가
}
return instance;
}
}
private void Awake()
{
// instance가 null이면 현재 객체를 Singleton으로 설정
if (instance == null)
instance = this as T;
OnAwake(); // Singleton을 상속받는 자식 클래스가 초기화 코드를 작성할 수 있도록 호출
}
public virtual void OnAwake() { }
}
>> 참고 영상
※ 게임이 시작되면 그냥 처음으로 불리우는 함수
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
public static void OnBeforeSceneLoadRuntimeMethod()
{
}
Strategy Pattern (전략 패턴)을 이용한 턴제 조종하기
Player Input에서 Actions 추가 (Number_1, Number_2)
Singleton + Observer Pattern
기본 입력 이벤트를 Interface로 정의
>> IReceiveInput.cs
using UnityEngine;
public interface IReceiveInput
{
public void OnTriggered(string action, bool isTrigger);
public void OnReadValue(string action, Vector2 value);
}
플레이어의 상태 및 입력 관리
>> PlayerController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : Singleton<PlayerController>
{
private Dictionary<string, GameObject> players = new();
private Dictionary<GameObject, IReceiveInput> inputs = new();
private GameObject currentPlayer;
IEnumerator Start()
{
yield return new WaitForSeconds(3.0f);
ChangePlayer("Number_1");
}
public void AddObserver(string key, GameObject obj)
{
players.Add(key, obj);
}
public void AddInputObserver(GameObject obj, IReceiveInput input)
{
inputs[obj] = input;
}
private bool ChangePlayer(string key)
{
if (players.TryGetValue(key, out var player))
{
currentPlayer = player;
return true;
}
return false;
}
public void OnTriggered(string action, bool isTrigger)
{
if (isTrigger && ChangePlayer(action))
return;
if (!currentPlayer)
return;
if (ContainsInput())
{
inputs[currentPlayer].OnTriggered(action, isTrigger);
}
}
public void OnReadValue(string action, Vector2 value)
{
if (!currentPlayer)
return;
if (ContainsInput())
{
inputs[currentPlayer].OnReadValue(action, value);
}
}
private bool ContainsInput()
{
return inputs.ContainsKey(currentPlayer) && inputs[currentPlayer] != null;
}
}
>> CustomTag.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CustomTag : MonoBehaviour
{
public string tag;
void Start()
{
PlayerController.Instance.AddObserver(tag, gameObject);
}
}
Unity Input System과 연동
>> MyInputManager.cs
: Input에 대한 관리 --> 매 프레임마다 입력된 값을 PlayerController에 있는 OnReadValue, OnTriggered를 통해 호출하여 실행한다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class MyInputManager : Singleton<MyInputManager>
{
private InputAction moveInput;
private InputAction jumpInput;
private InputAction number_1;
private InputAction number_2;
void Start()
{
PlayerInput playerInput = GetComponent<PlayerInput>();
moveInput = playerInput.actions["Move"];
jumpInput = playerInput.actions["Jump"];
number_1 = playerInput.actions["Number_1"];
number_2 = playerInput.actions["Number_2"];
}
void Update()
{
var playerController = PlayerController.Instance;
playerController.OnReadValue("Move", moveInput.ReadValue<Vector2>());
playerController.OnTriggered("Jump", jumpInput.triggered);
playerController.OnTriggered("Number_1", number_1.triggered);
playerController.OnTriggered("Number_2", number_2.triggered);
}
}
>> Blackboard_Default.cs
: MyInputManager.cs에서 Input을 관리하므로 moveInput과 jumpInput 삭제
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Blackboard_Default : MonoBehaviour, IBlackboardBase
{
public Animator animator;
public Rigidbody rigidbody;
public float moveSpeed = 3.0f;
public float jumpForce = 3.0f;
public void InitBlackboard()
{
animator = GetComponent<Animator>();
rigidbody = GetComponent<Rigidbody>();
}
}
캐릭터 움직임 제어
>> IdleState.cs
: Input을 관리하는 주체가 Blackboard에서 InputManager로 바뀌면서 전체적인 코드 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class IdleState : MonoBehaviour, IState, IReceiveInput
{
public StateMachine Fsm { get; set; }
public Blackboard_Default Blackboard { get; set; }
private bool jumpInputTriggered = false;
private Vector2 moveInput = Vector2.zero;
public void InitState(IBlackboardBase blackboard)
{
Blackboard = blackboard as Blackboard_Default;
}
public void Enter()
{
PlayerController.Instance.AddInputObserver(Fsm.gameObject, this);
PlayerController.Instance.AddInputObserver(Fsm.gameObject, this);
Blackboard.animator.CrossFade("Idles", 0.1f);
Blackboard.animator.SetFloat("Speed", 0.0f);
}
public void UpdateState(float deltaTime)
{
if (jumpInputTriggered && Blackboard.rigidbody.velocity.y == 0.0f)
{
Fsm.ChangeState<JumpState>();
return;
}
if (moveInput.sqrMagnitude > 0)
{
Fsm.ChangeState<WalkState>();
}
}
public void Exit()
{
PlayerController.Instance.AddInputObserver(Fsm.gameObject, null);
PlayerController.Instance.AddInputObserver(Fsm.gameObject, null);
jumpInputTriggered = false;
moveInput = Vector2.zero;
}
public void OnTriggered(string action, bool isTrigger)
{
if (action == "Jump")
jumpInputTriggered = isTrigger;
}
public void OnReadValue(string action, Vector2 value)
{
if (action == "Move")
moveInput = value;
}
}
>> WalkState.cs
: Input을 관리하는 주체가 Blackboard에서 InputManager로 바뀌면서 전체적인 코드 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class WalkState : MonoBehaviour, IState, IReceiveInput
{
public StateMachine Fsm { get; set; }
public Blackboard_Default Blackboard { get; set; }
private bool jumpInputTriggered = false;
private Vector2 moveInput = Vector2.zero;
public void InitState(IBlackboardBase blackboard)
{
Blackboard = blackboard as Blackboard_Default;
}
public void Enter()
{
PlayerController.Instance.AddInputObserver(Fsm.gameObject, this);
PlayerController.Instance.AddInputObserver(Fsm.gameObject, this);
Blackboard.animator.CrossFade("Idles", 0.1f);
Blackboard.animator.SetFloat("Speed", 1.0f);
}
public void UpdateState(float deltaTime)
{
if (jumpInputTriggered && Blackboard.rigidbody.velocity.y == 0.0f)
{
Fsm.ChangeState<JumpState>();
return;
}
if (0 >= moveInput.sqrMagnitude)
{
Fsm.ChangeState<IdleState>();
return;
}
Blackboard.rigidbody.velocity = new Vector3(moveInput.x * Blackboard.moveSpeed, Blackboard.rigidbody.velocity.y, moveInput.y * Blackboard.moveSpeed);
}
public void Exit()
{
PlayerController.Instance.AddInputObserver(Fsm.gameObject, null);
PlayerController.Instance.AddInputObserver(Fsm.gameObject, null);
jumpInputTriggered = false;
moveInput = Vector2.zero;
}
public void OnTriggered(string action, bool isTrigger)
{
}
public void OnReadValue(string action, Vector2 value)
{
if (action == "Move")
moveInput = value;
}
}
└ Unity Setting
>> MyChan 캐릭터 2개로 변경 및 Component 수정
: MyInputManager로 따로 만들어줬기 때문에 PlayerInput 제거
>> PlayerController, MyInputManager 추가
스스로 Enum 코드 만들어내기
: Attribute는 크게 신경 안 써도 된다. --> 할 줄 알면 회사에서 칭찬 받는다, 퇴근 시간이 빨라진다, 자동화 할 수 있다 정도
>> StateManagerGeneratorWindow.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using UnityEditor;
using UnityEngine;
using Directory = UnityEngine.Windows.Directory;
public class StateManagerGeneratorWindow : EditorWindow
{
[MenuItem("Tools/State Manager Generator")]
public static void ShowWindow()
{
GetWindow<StateManagerGeneratorWindow>("State Manager Generator");
}
private void OnGUI()
{
if (GUILayout.Button("Generate"))
{
Dictionary<string, List<Type>> enums = new();
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var assembly in assemblies)
{
var types = assembly.GetTypes().Where(t => t.GetCustomAttributes(typeof(StateAttribute), true).Length > 0);
foreach (var type in types)
{
var attr = type.GetCustomAttribute<StateAttribute>();
if (attr != null && !string.IsNullOrEmpty(attr.StateName))
{
if (!enums.ContainsKey(attr.StateName))
{
enums.Add(attr.StateName, new List<Type>());
}
enums[attr.StateName].Add(type);
}
}
}
var List = enums.Keys.OrderBy(s => s).ToList();
var savePath = "Assets/Scripts/Generated";
StringBuilder sb = new StringBuilder();
if (!Directory.Exists(savePath))
{
Directory.CreateDirectory(savePath);
}
sb.AppendLine("using System;");
sb.AppendLine("using System.Collections.Generic;");
sb.AppendLine($"public class StateTypesClasses");
sb.AppendLine("{");
GenerateFillEnumText(sb, List);
sb.AppendLine("\tprivate static readonly Dictionary<Type, StateTypes> TypeToState = new()");
sb.AppendLine("\t{");
foreach (var se in List)
{
var orderedList = enums[se].OrderBy(t => t.Name).ToList();
foreach (var type in orderedList)
{
sb.AppendLine($"\t\t[typeof({type})] = StateTypes.{se},");
}
}
sb.AppendLine("\t};");
sb.AppendLine("\tpublic static StateTypes GetState<T>() => GetState(typeof(T));");
sb.AppendLine("\tpublic static StateTypes GetState(Type type)");
sb.AppendLine("\t{");
sb.AppendLine("\t\treturn TypeToState.GetValueOrDefault(type, StateTypes.None);");
sb.AppendLine("\t}");
sb.AppendLine("}");
string filePath = Path.Combine(savePath, "StateTypesClasses.cs");
File.WriteAllText(filePath, sb.ToString());
}
}
private static void GenerateFillEnumText(StringBuilder sb, List<string> List)
{
sb.AppendLine($"\tpublic enum StateTypes");
sb.AppendLine("\t{");
sb.AppendLine("\t\tNone,");
foreach (var se in List)
{
sb.AppendLine($"\t\t{se},");
}
sb.AppendLine("\t\tMax");
sb.AppendLine("\t}");
}
}
>> StateAttribute.cs
using System;
[AttributeUsage(AttributeTargets.Class)]
public class StateAttribute : Attribute
{
public string StateName { get; private set; }
public StateAttribute(string stateName)
{
StateName = stateName;
}
}
>> StateMachine.cs
using System;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using STC = StateTypesClasses; // 추가
public enum StaterType
{
None,
Character,
Max
}
public static class StateFactory
{
public static List<IState> CreateStates(this StateMachine stateMachine, StaterType staterType)
{
List<IState> stateList = new List<IState>();
switch (staterType)
{
case StaterType.Character:
{
stateList.Add(stateMachine.AddComponent<IdleState>());
stateList.Add(stateMachine.AddComponent<WalkState>());
stateList.Add(stateMachine.AddComponent<JumpState>());
}
break;
}
return stateList;
}
}
public class StateMachine : MonoBehaviour
{
[SerializeField]private string defaultState;
private IState currentState;
private Dictionary<STC.StateTypes, IState> stateDict = new(); // 변경
public void Run()
{
IBlackboardBase blackboardBase = GetComponent<IBlackboardBase>();
blackboardBase.InitBlackboard();
List<IState> states = this.CreateStates(StaterType.Character);
foreach (var state in states)
{
AddState(state, blackboardBase);
}
ChangeState(Type.GetType(defaultState));
}
public void AddState(IState state, IBlackboardBase blackboard)
{
state.Fsm = this;
state.InitState(blackboard);
stateDict.Add(STC.GetState(state.GetType()), state); // 변경
}
public void ChangeState<T>() where T : IState
{
ChangeState(typeof(T));
}
public void ChangeState(Type stateType) // 변경
{
ChangeState(STC.GetState(stateType));
}
public void ChangeState(STC.StateTypes stateType) // 추가
{
currentState?.Exit();
if (!stateDict.TryGetValue(stateType, out currentState)) return;
currentState?.Enter();
}
public void UpdateState()
{
if (currentState != null)
currentState.UpdateState(Time.deltaTime);
}
}
>> 각 State 수정
- IdleState.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
[State("IdleState")] // 추가
public class IdleState : MonoBehaviour, IState, IReceiveInput
{
public StateMachine Fsm { get; set; }
public Blackboard_Default Blackboard { get; set; }
private bool jumpInputTriggered = false;
private Vector2 moveInput = Vector2.zero;
public void InitState(IBlackboardBase blackboard)
{
Blackboard = blackboard as Blackboard_Default;
}
public void Enter()
{
PlayerController.Instance.AddInputObserver(Fsm.gameObject, this);
PlayerController.Instance.AddInputObserver(Fsm.gameObject, this);
Blackboard.animator.CrossFade("Idles", 0.1f);
Blackboard.animator.SetFloat("Speed", 0.0f);
}
public void UpdateState(float deltaTime)
{
if (jumpInputTriggered && Blackboard.rigidbody.velocity.y == 0.0f)
{
Fsm.ChangeState<JumpState>();
return;
}
if (moveInput.sqrMagnitude > 0)
{
Fsm.ChangeState<WalkState>();
}
}
public void Exit()
{
PlayerController.Instance.AddInputObserver(Fsm.gameObject, null);
PlayerController.Instance.AddInputObserver(Fsm.gameObject, null);
jumpInputTriggered = false;
moveInput = Vector2.zero;
}
public void OnTriggered(string action, bool isTrigger)
{
if (action == "Jump")
jumpInputTriggered = isTrigger;
}
public void OnReadValue(string action, Vector2 value)
{
if (action == "Move")
moveInput = value;
}
}
- WalkState.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
[State("WalkState")] // 추가
public class WalkState : MonoBehaviour, IState, IReceiveInput
{
public StateMachine Fsm { get; set; }
public Blackboard_Default Blackboard { get; set; }
private bool jumpInputTriggered = false;
private Vector2 moveInput = Vector2.zero;
public void InitState(IBlackboardBase blackboard)
{
Blackboard = blackboard as Blackboard_Default;
}
public void Enter()
{
PlayerController.Instance.AddInputObserver(Fsm.gameObject, this);
PlayerController.Instance.AddInputObserver(Fsm.gameObject, this);
Blackboard.animator.CrossFade("Idles", 0.1f);
Blackboard.animator.SetFloat("Speed", 1.0f);
}
public void UpdateState(float deltaTime)
{
if (jumpInputTriggered && Blackboard.rigidbody.velocity.y == 0.0f)
{
Fsm.ChangeState<JumpState>();
return;
}
if (0 >= moveInput.sqrMagnitude)
{
Fsm.ChangeState<IdleState>();
return;
}
Blackboard.rigidbody.velocity = new Vector3(moveInput.x * Blackboard.moveSpeed, Blackboard.rigidbody.velocity.y, moveInput.y * Blackboard.moveSpeed);
}
public void Exit()
{
PlayerController.Instance.AddInputObserver(Fsm.gameObject, null);
PlayerController.Instance.AddInputObserver(Fsm.gameObject, null);
jumpInputTriggered = false;
moveInput = Vector2.zero;
}
public void OnTriggered(string action, bool isTrigger)
{
}
public void OnReadValue(string action, Vector2 value)
{
if (action == "Move")
moveInput = value;
}
}
- JumpState.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[State("JumpState")] // 추가
public class JumpState : MonoBehaviour, IState
{
public StateMachine Fsm { get; set; }
public Blackboard_Default Blackboard { get; set; }
public void InitState(IBlackboardBase blackboard)
{
Blackboard = blackboard as Blackboard_Default;
}
public void Enter()
{
Blackboard.animator.CrossFade("Jump", 0.1f);
Blackboard.rigidbody.velocity = new Vector3(Blackboard.rigidbody.velocity.x, Blackboard.jumpForce, Blackboard.rigidbody.velocity.z);
}
public void UpdateState(float deltaTime)
{
if (Blackboard.rigidbody.velocity.y == 0.0f)
{
Fsm.ChangeState<IdleState>();
}
}
public void Exit()
{
}
}
State Manager Generator 작동하기
- 작동하면 StateTypesClasses.cs가 자동으로 생성됨
>> StateTypesClasses.cs
using System;
using System.Collections.Generic;
public class StateTypesClasses
{
public enum StateTypes
{
None,
IdleState,
JumpState,
WalkState,
Max
}
private static readonly Dictionary<Type, StateTypes> TypeToState = new()
{
[typeof(IdleState)] = StateTypes.IdleState,
[typeof(JumpState)] = StateTypes.JumpState,
[typeof(WalkState)] = StateTypes.WalkState,
};
public static StateTypes GetState<T>() => GetState(typeof(T));
public static StateTypes GetState(Type type)
{
return TypeToState.GetValueOrDefault(type, StateTypes.None);
}
}
└ 문제 발생
: 코드는 다 그대로 작성했는데, StateAttribute.cs의 문제인건지 무언갈 놓친게 있는건지 스크립트 상에서 사진과 같이 에러가 발생한다.
※ IdleState.cs 뿐만 아니라 WalkState.cs, JumpState.cs도 마찬가지
그래서 State Manager Generator로 StateTypesClasses.cs를 생성해도 다음과 같이 생성된다.
using System;
using System.Collections.Generic;
public class StateTypesClasses
{
public enum StateTypes
{
None,
Max
}
private static readonly Dictionary<Type, StateTypes> TypeToState = new()
{
};
public static StateTypes GetState<T>() => GetState(typeof(T));
public static StateTypes GetState(Type type)
{
return TypeToState.GetValueOrDefault(type, StateTypes.None);
}
}
강사님 깃을 통해 프로젝트를 다운받아서 열어보면 아래 사진과 같이 문제 없이 작동한다.
사실 이 부분은 너무 어려워서 코드부터 이해하질 못 했기 때문에 문제 해결 방법도 잘 모르겠다..
Design Pattern, 참고할 Youtube 영상
※ Unity Korea에서 알려주는 패턴이 되게 설명을 잘 해준다!
>> 디자인 패턴의 기초, SOLID 원칙 이해하기
>> Singleton
>> Command Pattern
>> State Machine
'Development > C#' 카테고리의 다른 글
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 38일차 (1) | 2025.01.15 |
---|---|
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 32~37일차 (0) | 2025.01.15 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 30일차 (0) | 2025.01.11 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 27일차 (0) | 2024.12.27 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 26일차 (0) | 2024.12.26 |