내일배움캠프 39일차 6/11 TIL + Unity공부
개발 팁
씬을 로드하면 이전 씬은 재활용 안할 것이라면 제거 해줘야한다.
데이터관리 오브젝트와 화면에 보이는 오브젝트를 분리해라. 그러면 오브젝트를 숨기거나 없애고서도 해당 오브젝트의 데이터를 참조 가능
팀프로젝트 리펙토링
유니티 숙련주차 프로젝트의 몬스터들이 각각 스크립트로 이루어져있었는데,
Enemy 클래스를 만들어서 공통 기능들을 모아주니 훨씬 코드가 간결해졌다.
몬스터들을 어떻게 넣을지 계획을 안하고 즉석해서 하나하나 넣다보니 되게 코드가 불필요하게 길어졌던 것 같다.
다음에는 각 기능들도 인터페이스로 나누면 더 좋을 것이다.
Enemy :
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.AI;
public enum AIState
{
Idle,
Wandering,
Attacking
}
public class Enemy : MonoBehaviour, IDamagable, DropItem
{
[Header("Data")]
public int NPCID = 0;
public int MaxHealth;
protected int currentHealth;
public float walkSpeed;
public float runSpeed;
public ItemData[] itemToDrop;
[Header("AI")]
protected NavMeshAgent agent;
public float detectDistance;
protected AIState aiState;
[Header("Combat")]
public int damage;
public float attackRate;
protected float lastAttackTime;
public float attackDistance;
public float fieldOfView = 120f;
[Header("Components")]
protected Animator animator;
protected SkinnedMeshRenderer[] meshRenderer;
[Header("Others")]
protected float playerDistance;
protected virtual void Awake()
{
agent = GetComponent<NavMeshAgent>();
animator = GetComponent<Animator>();
meshRenderer = GetComponentsInChildren<SkinnedMeshRenderer>();
currentHealth = MaxHealth;
}
protected void Start()
{
SetState(AIState.Idle);
}
// �÷��̾���� �Ÿ��� ����ؼ� ����
protected virtual void Update()
{
playerDistance = Vector3.Distance(transform.position, CharacterManager.Instance.Player.transform.position);
switch (aiState)
{
case AIState.Idle:
case AIState.Wandering:
PassiveUpdate();
break;
case AIState.Attacking:
AttackingUpdate();
break;
}
}
// ���´�
// Idle = �ڸ����� ���
// Wandering = ��Ÿ �ൿ
// Attaking = �÷��̾ ���� ������ ������ �Ѿư��� ����
public void SetState(AIState state)
{
aiState = state;
switch (aiState)
{
case AIState.Idle:
agent.speed = walkSpeed;
agent.isStopped = true;
break;
case AIState.Wandering:
agent.speed = walkSpeed;
agent.isStopped = false;
break;
case AIState.Attacking:
agent.speed = runSpeed;
agent.isStopped = false;
break;
}
animator.speed = agent.speed / walkSpeed;
}
// ��� �ϴٰ� �÷��̾ ������ �߰�
protected virtual void PassiveUpdate()
{
if (playerDistance < detectDistance)
{
SetState(AIState.Attacking);
}
}
// ���� ���� ������ ����
// �������� ����� �߰�
// �߰� �Ұ����ϰų� �߰� ���� ���̸� ��Ÿ �ൿ
protected virtual bool AttackingUpdate()
{
if (playerDistance < attackDistance && IsPlayerInFieldOfView())
{
agent.isStopped = true;
if (Time.time - lastAttackTime > attackRate)
{
lastAttackTime = Time.time;
AttackMethod();
animator.speed = 1;
}
return true;
}
return false;
}
protected virtual void AttackMethod()
{
}
// ���� �տ� ������ ����
bool IsPlayerInFieldOfView()
{
Vector3 directionToPlayer = CharacterManager.Instance.Player.transform.position - transform.position;
float angle = Vector3.Angle(transform.forward, directionToPlayer);
return angle < fieldOfView * 0.5f;
}
public void TakePhysicalDamage(int damage)
{
currentHealth -= damage;
if (currentHealth <= 0)
{
Die();
}
StartCoroutine(DamageFlash());
}
void Die()
{
DropItem();
QuestManager.Instance.HuntMonster(NPCID);
Destroy(gameObject);
}
IEnumerator DamageFlash()
{
for (int i = 0; i < meshRenderer.Length; i++)
{
meshRenderer[i].material.color = new Color(1.0f, 0.6f, 0.6f);
}
yield return new WaitForSeconds(0.1f);
for (int i = 0; i < meshRenderer.Length; i++)
{
meshRenderer[i].material.color = Color.white;
}
}
public float GetHealthRatio()
{
return (float)currentHealth / MaxHealth;
}
public void DropItem()
{
foreach (ItemData item in itemToDrop)
{
Instantiate(item.dropPrefab, transform.position + new Vector3(1, 1), Quaternion.identity);
}
}
}
바뀐 NPC :
using System.Collections;
using UnityEngine;
using UnityEngine.AI;
public class NPC : Enemy
{
[Header("Wandering")]
public float minWanderDistance;
public float maxWanderDistance;
public float minWanderWaitTime;
public float maxWanderWaitTime;
public float maxWanderingMoveTime;
private float wanderingStartTime;
protected override void Update()
{
base.Update();
animator.SetBool("Moving", aiState != AIState.Idle);
}
protected override void PassiveUpdate()
{
base.PassiveUpdate();
if ((aiState == AIState.Wandering && agent.remainingDistance < 0.1f) || (Time.time - wanderingStartTime >= maxWanderingMoveTime))
{
SetState(AIState.Idle);
Invoke("WanderToNewLocation", Random.Range(minWanderWaitTime, maxWanderWaitTime));
}
}
protected override void AttackMethod()
{
base.AttackMethod();
CharacterManager.Instance.Player.Controller.GetComponent<IDamagable>().TakePhysicalDamage(damage);
}
void WanderToNewLocation()
{
if (aiState != AIState.Idle) { return; }
SetState(AIState.Wandering);
agent.SetDestination(GetWanderLocation());
wanderingStartTime = Time.time;
}
Vector3 GetWanderLocation()
{
NavMeshHit hit;
NavMesh.SamplePosition(transform.position + (Random.onUnitSphere * Random.Range(minWanderDistance, maxWanderDistance)), out hit, maxWanderDistance, NavMesh.AllAreas);
int i = 0;
while (Vector3.Distance(transform.position, hit.position) < detectDistance)
{
NavMesh.SamplePosition(transform.position + (Random.onUnitSphere * Random.Range(minWanderDistance, maxWanderDistance)), out hit, maxWanderDistance, NavMesh.AllAreas);
i++;
if (i == 30) { break; }
}
return hit.position;
}
protected override bool AttackingUpdate()
{
if (base.AttackingUpdate()) { return true; }
if (playerDistance < detectDistance)
{
agent.isStopped = false;
NavMeshPath path = new NavMeshPath();
agent.SetDestination(CharacterManager.Instance.Player.transform.position);
}
else
{
agent.SetDestination(transform.position);
agent.isStopped = true;
SetState(AIState.Wandering);
}
return false;
}
}