지금까지 구현한 총알 발사 로직은 실제 Bullet모델이 날아가서 몬스터와 충돌을 일으키는 Projectile방식으로 돼 있다. 대부분의 FPS게임에서는 실제 총알이 날라가지 않고 발사와 동시에 적에 명중해서 혈흔 효과와 같은 이벡트를 연출하고 적이 사망한다. 유니티에서 이러한 방식을 구현할수 있는 Raycast를 제공한다.
눈에 보이지 않는 광선을 발사해서 광선에 맞은 물체를 판단해서 후처리를 하는 방식이다. 다음과 같이 광선의 발사원점과 발사각, 거리등의 인자로 광선을 발사 할 수 있다.
이 기능은 게임스테이지에서 마우스 포인트 위치로 레이캐스트해서 3차원좌푯값을 읽어온후 해당 좌표로 이동시킬때도 사용한다.
DrawRay
레이캐스트는 씬 뷰에서 시각적으로 표시되지 않기 때문에 개발할 때는 DrawRay 함수를 이용해 시각적으로 표시하면서 개발을 해야한다. FireCtrl 스크립트의 Update()에서 Debug.DrawRay()를 추가한다.
void Update()
{ //마우스 왼쪽 버튼을 클릭했을때 Fire 함수 호출
Debug.DrawRay(firePos.position, firePos.forward * 10.0f, Color.green);
if(Input.GetMouseButtonDown(0)) {
Fire(); //발사처리
}
}
Play후 씬뷰탭을 눌러 보면 Ray가 잘 그려진다. 사실 좀 불편하다.
LineRenderer
게임뷰에서도 보일수 있게 하이라키뷰에서 Player를 선택하고 LineRendere 컴포넌트를 추가한다.
FireCtrl 스크립트 전역변수에 lr을 선언하고
private LineRenderer lr;
Start()에서 LineRenderer 컴포넌트를 연결해주고 선의 두께, 색을 지정해준다.
private void Start() {
//중략
lr = GetComponent<LineRenderer>();
lr.startWidth = 0.01f; //라인 시작 두께
lr.endWidth = 0.1f; //라인 시작 두께
lr.startColor = Color.red; //라인 시작 색깔
lr.endColor = Color.blue; //라인 종료 색깔
}
이제 Update()에서 시작점과 종료점을 지정해주면 실시간으로 선을 그려준다.
void Update()
{ //마우스 왼쪽 버튼을 클릭했을때 Fire 함수 호출
lr.SetPosition(0, firePos.position);
lr.SetPosition(1,firePos.position + firePos.forward * 20f - firePos.up);
Raycast, RaycastHit
레이캐스트가 객체를 검출하기 위해서는 그 객체는 Collider 컴포넌트를 갖고 있어야 한다. 특정 레이어만 감지하게 할 수도 있다. 몬스터는 6번째 MONSTER_BODY 레이어로 지정되어 있어 LayerMask(1<<6)를 사용 필터링 할 수 있다.
이제 물리적 총알은 시각적이고 Ray를 맞았을때 데미지를 입도록 수정해보자.
총알생성로직은 FireCtrl 스크립를 수정한다.
전역변수로 hit를 추가한다. Raycast발사후 충돌객체의 정보를 돌려준다.
private RaycastHit hit; // Raycast 결과값을 저장하기 위한 구조체
Variables
barycentricCoordinate | 충돌한 triangle의 무게중심 좌표 |
collider | 충돌한 collider |
distance | Ray의 origin으로부터 충돌 지점까지의 거리 |
lightmapCoord | 충돌 지점의 uv lightmap 좌표 |
normal | Ray가 충돌한 surface의 normal |
point | Ray가 충돌한 Collider의 충돌 지점 (world 좌표 사용) |
rigidbody | 충돌한 collider의 rigidbody (rigidbody가 없는 경우 null 반환) |
textureCoord | 충돌 위치에서의 uv texture 좌표 |
textureCoord2 | 충돌 위치에서의 2차 uv texture 좌표 |
transform | 충돌한 Transform의 Rigidbody 또는 Collider |
triangleIndex | 충돌한 triangle의 index |
이제 Physics.Raycast()를 이용해 눈에 보이지 않는 광선을 쏴보자 사용법은 여러가지가 있으나 다음 방법을 사용했다
Physics.Raycast(발사원점, 발사방향벡터, 결과값, 발사거리, 레이어마스크)
총구의 위치가 약간 위를 바라보고 있어 방향 layDir 을 약간 아래로 수정했다. 1<<6은 몬스터 레이어가 6번째이무로 시프트연산자를 사용해 오른쪽에서 6번째 비트를 켠것이다. 비트연산자를 사용해 복수의 레이어를 선택할 수도 있다.
void Update()
{ //마우스 왼쪽 버튼을 클릭했을때 Fire 함수 호출
//중략
Vector3 layDir = firePos.forward * 20.0f - firePos.up;
if (Physics.Raycast(firePos.position, layDir, out hit, 20.0f, 1 << 6)) {
}
교재에서는 Instantiate(bullet)를 마크했으나 필요에 따라 사용할수 있게 매개변수bool shot로 끄고 켤수 있게 했다. Update()안에서 Fire(false)로 잠시 총알 발사를 못하게 할 수 있다.
void Fire(bool shot) {
if(shot) {
GameObject b = Instantiate(bullet, firePos.position, firePos.rotation); //Bullet프리팹을 동적으로 발생
b.transform.Rotate(Vector3.right * 3f); //총탄의 각도가 높아서 낮추었음
}
audio.PlayOneShot(fireStx, 1.0f); //총소리발생\
StartCoroutine(ShowMuzzleFlash()); //총구화염효과 코루틴함수 호출
}
Fire(false)로 이제 총알이 나가지 않는다 Raycast()는 OnCollisionEnter나 OnTriggerEnter Event를 발생시키지 않는다. 따라서 다음과 같이 MonsterCtrl의 OncollisionEnter()의 피격효과및 HP관리를 지우고 OnDamage함수로 처리를 옮겨준다.
private void OnCollisionEnter(Collision coll) {
if (coll.collider.CompareTag("BULLET")) {
Destroy(coll.gameObject); //충돌한 총알을 삭제
}
}
public void OnDamage(Vector3 pos, Vector3 normal) {
anim.SetTrigger(hashHit); //피격 리액셔 애니메이션 실행
Quaternion rot = Quaternion.LookRotation(normal); //충돌지점 법선벡트
ShowBloodEffect(pos, rot); //혈흔효과를 생성하는 함수호출
hp -= 20;
Debug.Log(hp);
if (hp <= 0) {
state = State.DIE;
GameManager.instance.DisplayScore(50);
}
}
OnDamage를 유니티의 표준 콜백함수가 아니므로 FireCtrl 스크립트내 Update()안의 RaycastHit hit변수를 보고 충돌이 있었을때 충돌체의 MonsterCtrl스크립트 컴포넌트내의 OnDamage()함수를 호출한다.
if (Input.GetMouseButtonDown(0)) {
Fire(false); //true=총알발사처리, false=총소리만
Vector3 layDir = firePos.forward * 20.0f - firePos.up;
if (Physics.Raycast(firePos.position, layDir, out hit, 20.0f, 1 << 6)) {
//Debug.Log($"Hit={hit.transform.name}");
hit.transform.GetComponent<MonsterCtrl>()?.OnDamage(hit.point, hit.normal);
}
}
이제 물리적 총알 발사없이 타격효과및 Hp 관리가 문제없이 된다면 다시 위 코드에서 Fire(true)로 바꿔 물리적 총알 발사를 활성화한다.
if (Input.GetMouseButtonDown(0)) {
Fire(true); //true=총알발사처리, false=총소리만
#Debug.DrawRay #LineRenderer #RayCast #RayCastHit 3가지를 배우느라 좀 힘들수 있다. 실제 이 3가지를 정리하느라 몇칠 걸린듯 하니 천천히 이해해주시기 바랍니다.