



물리 엔진의 불필요한 부하는 특정 Collider 사이에 충돌이 감지되지 않게 설정 함으로써 줄일 수 있다. 따라서 다른 layer를 설정해서 부하를 줄일 수 있다.

Layer Collision Matrix 설정은 대표적인 예로 아군과 적군이 있을때 아군이 발사한 총알의 충돌은 적군에게만 발생하게 하는 로직에 응용하여 물리 엔진의 부하도 줄이고 로직도 간결하게 구현할 수 있다.
본 구조의 각 관절은 모두 각각의 게임 오브젝트 + Transform 컴포넌트를 가지고 있다. 많은 Transform 컴포넌트의 이동 및 회전 처리는 런타임 시 내부적으로 다양한 연산 처리를 하므로 최적화가 필요.
모델의 Optimize Game Objects를 한 다음 실제로 사용되는 양손 관절인 L_wrist, R_wrist만 노출되고 나머지 관절은 보이지 않도록 하자.

하이러키 뷰에서도 몬스터 오브젝트가 굉장히 간단해 졌다.
monster.SendMessage("OnPlayerDie", SendMessageOptions.DontRequireReceiver);
SendMessage 함수는 첫 번재 인자로 전달한 함수명과 동일한 함수가 해당 게임 오브젝트의 스크립트에 있으면 실행하라는 뜻이다.
DontRequireReceiver : 호출한 함수가 없더라도 함수가 없다는 메시지를 반환받지 않겠다는 옵션
위처럼 SendMessage 방식의 경우, 적 캐릭타가 아주 많은 상황이면 순차적인 호출 방법이 그리 효율적인 방법은 아니다.
순차적인 호출방식을 이벤트 구동 방식으로 바꿔보자. 이벤트란 특정한 조건을 만족하면 자동으로 알려주는 메시지를 의미하고. 이벤트가 발생하면 시스템에서 자동으로 이를 통보하고, 이벤트에 연결된 모든 캐릭터에서 정해진 동작을 수행하는 방식이다. 이러한 이벤트 구동은 for, foreach 구문을 사용해 순차적으로 호출하는 방식보다 메모리 사용 측면이나 구동 속도 측면에서 효율적이다.
델리게이트는 함수(메서드)를 참조하는 변수이다. 즉, 함수를 저장할 수 있는 일종의 변수, C++의 함수 포인터와 같은 의미이다. 델리게이트를 먼저 선언한 후에 사용가능하다.
PlayerCtrl.cs
public delegate void PlayerDieHandler();
public static event PlayerDieHandler OnPlayerDie;
void PlayerDie() {
Debug.Log("Player Die !");
/*
GameObject[] monsters = GameObject.FindGameObjectsWithTag("MONSTER");
foreach (GameObject monster in monsters) {
monster.SendMessage("OnPlayerDie", SendMessageOptions.DontRequireReceiver);
}
*/
OnPlayerDie();
}
먼저 델리게이트를 이용해 이벤트 함수의 원형을 선언해야 한다. 이벤트는 언제 호출될지 모르기 때문에 정적 변수 (static) 키워드를 사용하여 선언해준다.

그런 다음 PlayerDie 함수에서 OnPlayerDie 이벤트를 발생시킨다. 이제 MonsterCtrl에서 이 이벤트에 반응할 함수를 연결해주면 된다.
MonsterCtrl.cs
void OnEnable() {
PlayerCtrl.OnPlayerDie += this.OnPlayerDie;
}
void OnDisable() {
PlayerCtrl.OnPlayerDie -= this.OnPlayerDie;
}
위와 같은 방식으로 이벤트의 연결과 해지를 처리해준다.
OnEnable()과 OnDisable() 스크립트가 활성화되거나 비활성화될 때 수행되는 함수로서 이벤트의 연결과 해지는 반드시 이 함수에서 처리해야 한다.
이렇게 처리해주면 이전 방식과 차이 없이, 이벤트 구동 방식으로 전환했다.
Speed란 float형 파라미터를 만들어서 인스페겉의 파라미터를 체크 후에 Multiplier 매개변수로 넣어줄 수 있다.

anim.SetFloat(hashSpeed, Random.Range(0.8f, 1.2f));
anim.SetTrigger(hashPlayerDie);
이런 식으로 애니메이션 속도 조절이 개별적으로 가능하다.
유니티에서는 크게 3가지로 UI를 제공한다


UI Toolkit은 비교적 최신 UI 기술이기 때문에 관련 강의 영상이나 자료가 최신게 비교적으로 많다.
캔버스를 하나 만들면 EventSystem도 같이 생기는 EventSystem은 EventSystem, Standalone Input Module 컴포넌트를 포함하고 있다. EventSystem 컴포넌트가 없다면 UI 항목이 클릭 또는 터치와 같은 다양한 입력 이벤트에 반응하지 않는다.
Render Mode
UI 항목을 렌더링하기 위해 추가한 카메라는 기존의 Main Camera와 충돌이 없도록 반드시 Clear Flag, Culling Mask, Depth 속성을 적절히 설정해야 한다.
Image

UI Z-Order
UI는 하이러키 뷰의 순서로 Z-Order를 결정한다. 위에서 아래로 갈수록 Z-Order가 높게 설정되는 방식이다.
Navigation : 버튼이 여러 개 있을 경우 키보드로 포커스를 어떻게 이동시킬 것인가에 대한 속성

EventSystem의 FirstSelected에 Button을 연결해줘서 처음 포커스를 어느 버튼에 두고 시작할 것인지를 설정할 수 있다.
On Click
유니티 에디터에서 Button 별로 함수를 연결하는 방식은 UI가 복잡할 경우 매우 번잡스럽고 반복적인 작업으로 개발 능률이 떨어진다. 또한 런타임에 생성되는 버튼에는 이벤트를 연결할 방법이 없다.
따라서 UnityEvent 또는 델리게이트를 사용한다.
UnityEvent는 유니티 에디터에서 편하게 사용하기 위해 미리 정의된 델리게이트일 뿐이다.
using UnityEngine.Events;
using UnityEngine.UI; // UnityEvent 관련 API를 사용하기 위한 네임스페이스
public Button startButton;
public Button optionButton;
public Button shopButton;
private UnityAction action;
void Start() {
// UnityAction을 사용한 이벤트 연결 방식
action = () => OnButtonClick(startButton.name);
startButton.onClick.AddListener(action);
// 무명 메서드를 활용한 이벤트 연결 방식
optionButton.onClick.AddListener(delegate {OnButtonClick(optionButton.name);});
// 람다식을 활용한 이벤트 연결 방식
shopButton.onClick.AddListener(() => OnButtonClick(shopButton.name));
}
무명 메서드 방식을 람다식 문법으로 사용한 경우는 단순히 무명 메서드 방식을 간략화한 방식이라고 보면 된다.
Text Mesh Pro를 임포트하고 폰트를 다운받아서 Window > TextMesh Pro > Font Asset Creator를 통해 Font Atlas 이미지를 생성해서 저장해야 한다.

한글은 11,172자 이기 때문에 Atals Resolution은 최소한 4096*4096을 선택해야한다.
하지만 저사양 모바일 기기에서 구동해야 하거나 텍스처 크기를 줄여야 할 때는 2350자를 사용한다. 한글 2350자는 KSX1001 규격으로 한국 산업 표준이다.
아래 사진과 같이 Cutom Character list에 사용할 KSX1001 글자를 입력해주고, Atlas Resolution을 2048*2048로 줄여준다. (Packing Method도 Optimum으로 설정해주면 인코딩 시간은 더 걸려도 텍스처 공간을 최적화해준다.)
글꼴을 저장한 Atals 이미지, SDF(Signed Distance Field)로 변경해주면 아래와 같이 한글이 잘 나온다.

TMP는 Text에 마크업 언어 사용이 가능하다
hpBar = GameObject.FindGameObjectWithTag("HP_BAR")?.GetComponent<Image>();
다음과 같이 문법에 ? 연산자를 써서 null 체크를 간단하게 할 수 있다. null이 아니면 뒤에 있는 구문을 실행해서 GetComponent 함수가 실행되고 null이면 null 값을 반환한다.
추가적인 업데이트가 있는 코드만 포함되어 있습니다.
private Image hpBar;
public delegate void PlayerDieHandler();
public static event PlayerDieHandler OnPlayerDie;
IEnumerator Start()
{
hpBar = GameObject.FindGameObjectWithTag("HP_BAR")?.GetComponent<Image>();
currHP = initHP;
tr = GetComponent<Transform>();
anim = GetComponent<Animation>();
anim.Play("Idle");
turnSpeed = 0.0f;
yield return new WaitForSeconds(0.3f);
turnSpeed = 80.0f;
}
void OnTriggerEnter(Collider coll) {
if (currHP >= 0.0f && coll.CompareTag("PUNCH")) {
currHP -= 10.0f;
DisplayHealth();
Debug.Log($"Plater hp = {currHP/initHP}");
if (currHP < 0.0f) {
PlayerDie();
}
}
}
void PlayerDie() {
Debug.Log("Player Die !");
/*
GameObject[] monsters = GameObject.FindGameObjectsWithTag("MONSTER");
foreach (GameObject monster in monsters) {
monster.SendMessage("OnPlayerDie", SendMessageOptions.DontRequireReceiver);
}
*/
OnPlayerDie();
}
void DisplayHealth() {
hpBar.fillAmount = currHP/initHP;
}
void OnEnable() {
PlayerCtrl.OnPlayerDie += this.OnPlayerDie;
}
void OnDisable() {
PlayerCtrl.OnPlayerDie -= this.OnPlayerDie;
}
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
public class UIManager : MonoBehaviour
{
public Button startButton;
public Button optionButton;
public Button shopButton;
private UnityAction action;
void Start() {
action = () => OnButtonClick(startButton.name);
startButton.onClick.AddListener(action);
optionButton.onClick.AddListener(delegate {OnButtonClick(optionButton.name);});
shopButton.onClick.AddListener(() => OnButtonClick(shopButton.name));
}
public void OnButtonClick(string msg) {
Debug.Log($"Click Button : {msg}");
}
}
IEnumerator MonsterAction() {
while(!isDie) {
switch (state) {
case State.IDLE :
agent.isStopped = true;
anim.SetBool(hashTrace, false);
break;
case State.TRACE :
agent.SetDestination(playerTr.position);
agent.isStopped = false;
anim.SetBool(hashTrace, true);
anim.SetBool(hashAttack, false);
break;
case State.ATTACK :
anim.SetBool(hashAttack, true);
break;
case State.DIE :
isDie = true;
agent.isStopped = true;
anim.SetTrigger(hashDie);
GetComponent<CapsuleCollider>().enabled = false;
break;
}
yield return new WaitForSeconds(0.3f);
}
}
void OnCollisionEnter(Collision coll) {
if (coll.collider.CompareTag("BULLET")) {
Destroy(coll.gameObject);
anim.SetTrigger(hashHit);
Vector3 pos = coll.GetContact(0).point;
Quaternion rot = Quaternion.LookRotation(-coll.GetContact(0).normal);
ShowBloodEffect(pos, rot);
hp -= 10;
if (hp <= 0) {
state = State.DIE;
}
}
}
void OnPlayerDie() {
StopAllCoroutines();
agent.isStopped = true;
anim.SetFloat(hashSpeed, Random.Range(0.8f, 1.2f));
anim.SetTrigger(hashPlayerDie);
}