이번에 소개할 클래스는
Bash 기술의 목표가 될 무기(Weapon)와 관련된 클래스입니다.
Weapon Class는 다음을 요구합니다.
1. Weapon의 상태(던져진 상태 , 획득 가능 상태)를 관리할 클래스가 존재해야함
2. Weapon은 플레이어가 감지하여 무기를 획득할 수 있어야 함.
가장 먼저 Weapon Class를 구현하여 상태를 관리합니다.
using Retro.ThirdPersonCharacter;
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class Weapon : MonoBehaviour
{
[SerializeField] private Material[] _weaponMat = new Material[4];
private WeaponCollider _weaponCollider;
private Rigidbody _rigidbody;
private bool _isThrown = false;
public bool isThrown { get => _isThrown; }
private void Start()
{
_rigidbody = GetComponent<Rigidbody>();
_weaponCollider = GetComponentInChildren<WeaponCollider>();
}
public void DestroyWeapon()
{
if (_weaponCollider.BoolCanPickUp)
{
Destroy(gameObject);
}
}
public void ChangeBoolThrown()
{
_isThrown = !_isThrown;
}
}
Weapon Class는 어떠한 무기라도 적용이 가능하게끔 간단하게 구현합니다.
Weapon의 획득 가능 상태를 판별하는 감지 기능은 WeaponCollider 클래스로 분리해서 구현합니다.
여기서 저는 Unity에서 제공하는 일반적인 Collider를 사용하지 않고,
LayCast를 활용하여 감지 기능을 구현하기로 했습니다.
LayCast를 사용한 이유는 2가지 입니다.
1. 벽 너머에 존재하는 무기는 획득할 수 없게 하는 기능을 구현하기 위해서
2. 많은 수의 Sphere Collider Trigger가 Physics에 영향을 받아 움직일 경우 Unity에서 성능 저하가 일어나기 때문.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static UnityEditor.PlayerSettings;
public class WeaponCollider : MonoBehaviour
{
[SerializeField] private float _collisionLength = 5.0f;
[SerializeField] private float _collisionHeight = 0.0f;
[SerializeField] private int horizontalRayCount = 0;
[SerializeField] private int verticalRayCount = 0;
private bool _bDetectCharacter = false;
private bool _bCanPickUp = false;
private Color _rayColor = Color.red;
private Vector3 collisionPos = Vector3.zero;
private Vector3 sphereCastDirVector = Vector3.zero;
private Vector3 gizmoDirVector = Vector3.zero;
private Vector3 hitVector = Vector3.zero;
public bool BoolCanPickUp { get => _bCanPickUp; }
void Start()
{
_bDetectCharacter = false;
_bCanPickUp = false;
sphereCastDirVector = Vector3.down;
gizmoDirVector = Vector3.down;
collisionPos = transform.position;
collisionPos.y += _collisionHeight;
}
// Update is called once per frame
void Update()
{
GetBoolWeaponPickUp();
}
private void GetBoolWeaponPickUp()
{
collisionPos = transform.position;
collisionPos.y += _collisionHeight;
for (int a = 0; a < verticalRayCount; a++)
{
for (int i = 0; i < horizontalRayCount; i++)
{
if (Physics.SphereCast(collisionPos, 0.01f, sphereCastDirVector, out RaycastHit hit, _collisionLength))
{
if (hit.transform.gameObject.tag == "Player")
{
_bDetectCharacter = true;
hitVector = hit.transform.position - collisionPos;
}
}
sphereCastDirVector = Quaternion.AngleAxis(360 / horizontalRayCount, Vector3.up) * sphereCastDirVector;
}
sphereCastDirVector = Quaternion.AngleAxis(180 / verticalRayCount, Vector3.right) * sphereCastDirVector;
}
if (_bDetectCharacter)
{
_bCanPickUp = true;
_bDetectCharacter = false;
}
else
_bCanPickUp = false;
sphereCastDirVector = Vector3.down;
}
private void CheckBashColliderDetect()
{
if (_bBashStay)
{
_bCanBash = true;
_bBashStay = false;
}
else
{
_bCanBash = false;
}
}
void OnDrawGizmos()
{
Gizmos.color = _rayColor;
float sphereScale = Mathf.Max(transform.lossyScale.x, transform.lossyScale.y, transform.lossyScale.z);
for (int a = 0; a < verticalRayCount; a++)
{
for (int s = 0; s < horizontalRayCount; s++)
{
// 함수 파라미터 : 현재 위치, Sphere의 크기(x,y,z 중 가장 큰 값이 크기가 됨), Ray의 방향, RaycastHit 결과, Sphere의 회전값, SphereCast를 진행할 거리
if (Physics.SphereCast(collisionPos, 0.01f, gizmoDirVector, out RaycastHit hit, _collisionLength))
{
if (hit.transform.gameObject.tag == "Player")
{
// Hit된 지점까지 ray를 그려준다.
Gizmos.DrawRay(collisionPos, transform.forward * hit.distance);
// Hit된 지점에 Sphere를 그려준다.
Gizmos.DrawWireSphere(collisionPos + hitVector, sphereScale / 2.0f);
}
}
else
{
// Hit가 되지 않았으면 최대 검출 거리로 ray를 그려준다.
Gizmos.DrawRay(collisionPos, gizmoDirVector * _collisionLength);
}
gizmoDirVector = Quaternion.AngleAxis(360 / horizontalRayCount, Vector3.up) * gizmoDirVector;
}
gizmoDirVector = Quaternion.AngleAxis(180 / verticalRayCount, Vector3.right) * gizmoDirVector;
}
gizmoDirVector = Vector3.down;
}
}
코드의 원리는 간단한 편입니다.
Weapon의 Transform을 중심으로 쏘아진 SphereCast가 Player를 감지하면
획득 가능 상태를 담당하는 bool (_bCanPickUp)을 변화시킵니다.
이제 획득 가능 상태가 된 무기를 획득하는 기능을 작성해야겠지요.
그런데 여기서 생각해야 할 것이 있습니다.
복수의 무기를 획득할 수 있는 상황이라면
Player는 가장 가까운 무기 1개를 획득해야만 합니다.
왜냐하면 무기는 한 번에 1개만 획득할 수 있기 때문입니다.
이를 위해선 획득할 수 있는 가장 가까운 Weapon을 찾는 기능이 필요합니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ManageWeapon : MonoBehaviour
{
[SerializeField] private GameObject _weapon;
[SerializeField] private Transform _firePos;
private GameObject copiedWeapon;
private Combat _combat;
private PlayerInput _playerInput;
private Collider[] closeWeaponArr;
private Weapon closeWeapon = null;
[SerializeField]private float weaponPickUpDistance = 5.0f;
private float weaponShortestDistance = 0.0f;
private bool _isHaveWeapon = true;
public bool IsHaveWeapon { get => _isHaveWeapon; }
private void Start()
{
_combat = GetComponent<Combat>();
_playerInput = GetComponent<PlayerInput>();
}
private void LateUpdate()
{
if (_combat.ThrowTrigger)
{
OnThrow();
_combat.ChangeBoolThrowTrigger();
}
if (_playerInput.WeaponPickUpInput)
{
FindWeapon();
}
}
private void OnThrow()
{
CreateWeapon();
_isHaveWeapon = !_isHaveWeapon;
}
private void CreateWeapon()
{
copiedWeapon = Instantiate(_weapon, _firePos.position, _firePos.rotation);
copiedWeapon.transform.rotation = _firePos.rotation;
copiedWeapon.GetComponent<Weapon>().ChangeBoolThrown();
}
private void FindWeapon()
{
if (_isHaveWeapon)
{
Debug.Log("youHaveWeapon");
return;
}
closeWeaponArr = Physics.OverlapSphere(transform.position, weaponPickUpDistance , 1 << LayerMask.NameToLayer("Weapon"));
Debug.Log(closeWeaponArr.Length);
if (closeWeaponArr.Length > 0)
{
Debug.Log("Found Weapon");
closeWeapon = closeWeaponArr[0].transform.GetComponent<Weapon>();
weaponShortestDistance = Vector3.Distance(transform.position, closeWeapon.transform.position);
for (int i = 0; i < closeWeaponArr.Length; i++)
{
if (Vector3.Distance(transform.position , closeWeaponArr[i].transform.position) < weaponShortestDistance)
{
closeWeapon = closeWeaponArr[i].transform.GetComponent<Weapon>();
weaponShortestDistance = Vector3.Distance(transform.position, closeWeapon.transform.position);
}
}
}
if (closeWeapon != null)
{
Debug.Log(closeWeapon.transform.position);
if (closeWeapon.GetComponentInChildren<WeaponCollider>().BoolCanPickUp)
{
PickUpWeapon(closeWeapon);
Debug.Log("PickUpWeaponSuccesfully");
}
else
{
Debug.Log("error : Weapon , BoolCanPickUp is false");
}
}
else
{
Debug.Log("closeWeapon is NUll");
}
}
private void PickUpWeapon(Weapon weapon)
{
_isHaveWeapon = !_isHaveWeapon;
weapon.DestroyWeapon();
closeWeapon = null;
closeWeaponArr = null;
}
}
Player 클래스 설계 (3)에서 구현한 Manage Weapon Class에 구문을 추가합니다.
이 중 FindWeapon 함수를 주목해주시길 바랍니다.
FindWeapon 함수는 Player가 무기를 획득하는 Key를 입력했을 때 동작하는 함수입니다.
Physics.OverlapSphere를 활용하여 Player 주위에 있는 Weapon을 CloseWeaponArr에 저장하고
이 중에서 bWeaponPickUp Boolean이 True인 무기를 판별합니다.
만약 획득할 수 있는 무기가 여럿 존재한다면,거리를 비교하여
Sort를 통해 가장 가까운 무기(closeWeapon)를 탐색합니다.
무기 획득이 완료되 해당 무기를 Destroy합니다.
추가적으로 ManageWeapon Class에 존재하는 OnThrow 함수는
Combat Class의 ThrowTrigger가 발동하면
Player의 앞 부분에서 Weapon을 생성합니다.
public class ManageWeapon : MonoBehaviour{
.
.
.
.
private void LateUpdate()
{
if (_combat.ThrowTrigger)
{
OnThrow();
_combat.ChangeBoolThrowTrigger();
}
}
private void OnThrow()
{
CreateWeapon();
_isHaveWeapon = !_isHaveWeapon;
}
private void CreateWeapon()
{
copiedWeapon = Instantiate(_weapon, _firePos.position, _firePos.rotation);
copiedWeapon.transform.rotation = _firePos.rotation;
copiedWeapon.GetComponent<Weapon>().ChangeBoolThrown();
}
}