TIL (since 2023.08.07 ~ )
2023-10-11(유니티 심화 주차 강의 4일차)
Bastian바스티언
2023. 10. 11. 22:33
유니티 심화 주차 실습
아이템 장착과 모션(Code)
<Equip + EquipManager + EquipTool>
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Equip : MonoBehaviour
{
public virtual void OnAttackInput(PlayerConditions conditions)
{
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class EquipManager : MonoBehaviour
{
public Equip curEquip;
public Transform equipParent;
private PlayerController controller;
private PlayerConditions conditions;
// singleton
public static EquipManager Instance;
private void Awake()
{
Instance = this;
controller = GetComponent<PlayerController>();
conditions = GetComponent<PlayerConditions>();
}
public void OnAttackInput(InputAction.CallbackContext context)
{
if(context.phase == InputActionPhase.Performed && curEquip != null && controller.canLook)
{
curEquip.OnAttackInput(conditions);
}
}
public void EquipNew(ItemData item)
{
UnEquip();
curEquip = Instantiate(item.equipPrefab, equipParent).GetComponent<Equip>();
}
public void UnEquip()
{
if(curEquip != null)
{
Destroy(curEquip.gameObject);
curEquip = null;
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EquipTool : Equip
{
public float attackRate;
private bool attacking;
public float attackDistance;
public float useStamina;
[Header("Resource Gathering")]
public bool doesGatherResources;
[Header("Combat")]
public bool doesDealDamage;
public int damage;
private Animator animator;
private Camera camera;
private void Awake()
{
camera = Camera.main;
animator = GetComponent<Animator>();
}
public override void OnAttackInput(PlayerConditions conditions) // 실제 공격 구현된 부분 (Equiptool 이 받아서 공격을 구현)
{
if(!attacking)
{
if (conditions.UseStamina(useStamina))
{
attacking = true;
animator.SetTrigger("Attack");
Invoke("OnCanAttack", attackRate);
}
}
}
void OnCanAttack()
{
attacking = false;
}
public void OnHit()
{
Ray ray = camera.ScreenPointToRay(new Vector3(Screen.width / 2, Screen.height / 2, 0));
RaycastHit hit;
if(Physics.Raycast(ray, out hit, attackDistance))
{
if(doesGatherResources && hit.collider.TryGetComponent(out Resource resource))
{
resource.Gather(hit.point, hit.normal);
}
if(doesDealDamage && hit.collider.TryGetComponent(out IDamagable damageable))
{
damageable.TakePhysicalDamage(damage);
}
}
}
}
그 외 팁들
- 애니메이션 창 단축키 : Ctrl + 6
- 애니메이터 설정시 시작과 끝은 항상 원점 유지
자원 채취(Code)
<Resource>
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Resource : MonoBehaviour
{
public ItemData itemToGive;
public int quantityPerHit = 1;
public int capacity;
public void Gather(Vector3 hitPoint, Vector3 hitNormal)
{
for (int i = 0; i < quantityPerHit; i++)
{
if (capacity <= 0) { break; }
capacity -= 1;
Instantiate(itemToGive.dropPrefab, hitPoint + Vector3.up, Quaternion.LookRotation(hitNormal, Vector3.up));
}
if (capacity <= 0)
Destroy(gameObject);
}
}
적 생성과 로직(Code)
<NPC>
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.AI;
public enum AIState
{
Idle,
Wandering,
Attacking,
Fleeing
}
public class NPC : MonoBehaviour, IDamagable
{
[Header("Stats")]
public int health;
public float walkSpeed;
public float runSpeed;
public ItemData[] dropOnDeath;
[Header("AI")]
private AIState aiState;
public float detectDistance;
public float safeDistance;
[Header("Wandering")]
public float minWanderDistance;
public float maxWanderDistance;
public float minWanderWaitTime;
public float maxWanderWaitTime;
[Header("Combat")]
public int damage;
public float attackRate;
private float lastAttackTime;
public float attackDistance;
private float playerDistance;
public float fieldOfView = 120f;
private NavMeshAgent agent;
private Animator animator;
private SkinnedMeshRenderer[] meshRenderers;
private void Awake()
{
agent = GetComponent<NavMeshAgent>();
animator = GetComponentInChildren<Animator>();
meshRenderers = GetComponentsInChildren<SkinnedMeshRenderer>();
}
private void Start()
{
SetState(AIState.Wandering);
}
private void Update()
{
playerDistance = Vector3.Distance(transform.position, PlayerController.instance.transform.position);
animator.SetBool("Moving", aiState != AIState.Idle);
switch (aiState)
{
case AIState.Idle: PassiveUpdate(); break;
case AIState.Wandering: PassiveUpdate(); break;
case AIState.Attacking: AttackingUpdate(); break;
case AIState.Fleeing: FleeingUpdate(); break;
}
}
private void FleeingUpdate()
{
if (agent.remainingDistance < 0.1f)
{
agent.SetDestination(GetFleeLocation());
}
else
{
SetState(AIState.Wandering);
}
}
private void AttackingUpdate()
{
if (playerDistance > attackDistance || !IsPlaterInFireldOfView())
{
agent.isStopped = false;
NavMeshPath path = new NavMeshPath();
if (agent.CalculatePath(PlayerController.instance.transform.position, path))
{
agent.SetDestination(PlayerController.instance.transform.position);
}
else
{
SetState(AIState.Fleeing);
}
}
else
{
agent.isStopped = true;
if (Time.time - lastAttackTime > attackRate)
{
lastAttackTime = Time.time;
PlayerController.instance.GetComponent<IDamagable>().TakePhysicalDamage(damage);
animator.speed = 1;
animator.SetTrigger("Attack");
}
}
}
private void PassiveUpdate()
{
if (aiState == AIState.Wandering && agent.remainingDistance < 0.1f)
{
SetState(AIState.Idle);
Invoke("WanderToNewLocation", Random.Range(minWanderWaitTime, maxWanderWaitTime));
}
if (playerDistance < detectDistance)
{
SetState(AIState.Attacking);
}
}
bool IsPlaterInFireldOfView()
{
Vector3 directionToPlayer = PlayerController.instance.transform.position - transform.position;
float angle = Vector3.Angle(transform.forward, directionToPlayer);
return angle < fieldOfView * 0.5f;
}
private void SetState(AIState newState)
{
aiState = newState;
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;
case AIState.Fleeing:
{
agent.speed = runSpeed;
agent.isStopped = false;
}
break;
}
animator.speed = agent.speed / walkSpeed;
}
void WanderToNewLocation()
{
if (aiState != AIState.Idle)
{
return;
}
SetState(AIState.Wandering);
agent.SetDestination(GetWanderLocation());
}
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;
}
Vector3 GetFleeLocation()
{
NavMeshHit hit;
NavMesh.SamplePosition(transform.position + (Random.onUnitSphere * safeDistance), out hit, maxWanderDistance, NavMesh.AllAreas);
int i = 0;
while (GetDestinationAngle(hit.position) > 90 || playerDistance < safeDistance)
{
NavMesh.SamplePosition(transform.position + (Random.onUnitSphere * safeDistance), out hit, maxWanderDistance, NavMesh.AllAreas);
i++;
if (i == 30)
break;
}
return hit.position;
}
float GetDestinationAngle(Vector3 targetPos)
{
return Vector3.Angle(transform.position - PlayerController.instance.transform.position, transform.position + targetPos);
}
public void TakePhysicalDamage(int damageAmount)
{
health -= damageAmount;
if (health <= 0)
Die();
StartCoroutine(DamageFlash());
}
void Die()
{
for (int x = 0; x < dropOnDeath.Length; x++)
{
Instantiate(dropOnDeath[x].dropPrefab, transform.position + Vector3.up * 2, Quaternion.identity);
}
Destroy(gameObject);
}
IEnumerator DamageFlash()
{
for (int x = 0; x < meshRenderers.Length; x++)
meshRenderers[x].material.color = new Color(1.0f, 0.6f, 0.6f);
yield return new WaitForSeconds(0.1f);
for (int x = 0; x < meshRenderers.Length; x++)
meshRenderers[x].material.color = Color.white;
}
}
<버그 리포트>
"GetRemainingDistance" can only be called on an active agent that has been placed on a NavMesh.
해당 에러가 났던 이유는, 월드맵에서 bake를 하지 않은 채 진행을 했기 때문에,
곰이 바닥에서 행동을 취할 수 없었기 때문이다.
Navigation Static 에 체크표시를 해준 뒤 bake를 해서 저장을 해주면 완료
포스트 프로세싱
포스트 프로세싱이란?
화면에 그려져 있는 것들에 대해서(만들어져 있는 것) 후처리 연산을 추가하여 연출을 넣어주는 것.
포스트 프로세싱(Post-Processing)은 Unity나 다른 게임 엔진에서 화면에 렌더링된 이미지에 추가적인 효과를 적용하는 기술입니다. 이 기술을 사용하여 게임 화면에 색상 보정, 블러 효과, 광학 효과 등을 적용하여 시각적으로 더 흥미로운 그래픽 효과를 구현할 수 있습니다.
포스트 프로세싱의 일부 기능과 사용법은 다음과 같습니다:
1. 색상 보정 및 색감 필터링:
화면에 특정 색조, 채도, 밝기 등의 보정을 적용하여 화면의 색상을 조작할 수 있습니다.
2. 블러 효과:
게임 화면에 블러 효과를 적용하여 광학적 흐림, 광학 흐림, 도형 흐림 등을 구현할 수 있습니다.
3. 선 처리 및 톤 매핑:
경계선 처리, 윤곽선 강조, 톤 매핑 등을 적용하여 게임 화면을 더 생동감 있게 만들 수 있습니다.
4. 광학 효과:
렌즈 반사, 빛 착란, 먼지 효과 등을 추가하여 게임 화면에 현실적인 광학 효과를 부여할 수 있습니다.
5. 사용자 정의 효과:
Unity의 포스트 프로세싱 스택을 사용하여 사용자가 직접 쉐이더 코드를 작성하여 원하는 효과를 만들어 적용할 수도 있습니다.