Chapter 12-2. 기타 : 포탑(Defense Tower) 만들기
카테고리: Unity Lesson 3
태그: Unity Game Engine
인프런에 있는 케이디님의 [유니티 3D] 실전! 생존게임 만들기 - Advanced 강의를 듣고 정리한 필기입니다. 😀
🌜 강의 들으러 가기 Click
🚀 포탑 오브젝트
평소에는 360도로 혼자 회전하다가 포신이 향하는 방향과 포탑에서 플레이어를 향하는 방향이 일치하면 플레이어를 쏜다.
TopGun
은 포신이다. Foundation
은 포탑의 몸체이다. Foundation
은 플레이어가 관통하지 못하도록 Collider를 붙여주었다.
총의 메시가 되는 SubMachineGun2
은 원래 땅에 눕혀져있던 모양이 디폴트였어서 x 축 중심으로 -90도 회전해주어 일으켰다. 그래서 이렇게 포신의 앞(Z축)이 하늘을 향하는 모양새가 되었다. 포신이 향하는 방향과 포탑에서 플레이어를 향하는 방향이 일치하면 플레이어를 쏘게 할 것이기 때문에 포신이 Z 축과 일치했으면 좋겠다.
그래서 SubMachineGun2
을 빈 오브젝트 GunHolder
의 자식으로 넣어주었다. 이로써 X = -90 회전값을 가지는 SubMachineGun2
가 부모인 GunHolder
의 X 축을 중심으로 -90도 회전한 셈이 되었기 때문에 GunHolder
의 회전축은 다음과 같이 된다. 아직도 Z
축이 포신과 일치하지 않았다. 여기서 회전 축 자체가 Y 축 중심으로 90도 더 회전되야 Z 축이 포신과 일치한다. 따라서 Y 축을 중심으로 -90도 회전해주고 TopGun
빈 오브젝트의 자식으로 이를 또 넣어준다.
그러면 Y = -90 회전값을 가지는 GunHolder
가 부모인 TopGun
의 Y 축을 중심으로 -90도 회전한 셈이 되었기 때문에 TopGun
의 회전축은 위와 같이 된다. 이제 포신의 방향과 Z 축이 일치하게 되었다.
🚀 포탑 애니메이션
✈ 애니메이션 클립
발포할 때 애니메이션은 6프레임 동안 살짝 총이 뒤로 밀려나왔다가 다시 제자리로 오게끔
정지 상태 애니메이션
✈ 애니메이션 컨트롤러
- 플레이어를 발견하면 발사 애니메이션 재생
- “Fire” Trigger 발동
- 발사 애니메이션 재생이 끝나면 자연스레 정지 애니메이션 재생
- 전이 조건 X, Has Exit Time 체크
🚀 포탑 작동시키기
📜DefenseTower
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DefenseTower : MonoBehaviour
{
[SerializeField] private string towerName; // 방워타워의 이름
[SerializeField] private float range; // 방어타워의 사정거리
[SerializeField] private int damage; // 방어 타워의 공격력
[SerializeField] private float rateOfAccuracy; // 정확도(0에 가까울 수록 정확도 높음)
[SerializeField] private float rateOfFire; // 연사속도(rateOfFire초마다 발사)
private float currentRateOfFire; // 연사속도 계산(갱신됨)
[SerializeField] private float viewAngle; // 시야각
[SerializeField] private float spinSpeed; // 포신 회전 속도
[SerializeField] private LayerMask layerMask; // 움직이는 대상만 타겟으로 지정(플레이어 혹은 동물)
[SerializeField] private Transform tf_TopGun; // 포신
[SerializeField] private ParticleSystem particle_MuzzleFlash; // 총구 섬광
[SerializeField] private GameObject go_HitEffect_Prefab; // 적중 효과 이펙트
private RaycastHit hitInfo;
private Animator anim;
private AudioSource theAudio;
private bool isFindTarget = false; // 적 타겟 발견시 True
private bool isAttack = false; // 정확히 타겟을 향해 포신 회전 완료시 True (총구 방향과 적 방향이 일치할 때)
private Transform tf_Target; // 현재 설정된 타겟의 트랜스폼
[SerializeField] private AudioClip sound_Fire;
void Start()
{
theAudio = GetComponent<AudioSource>();
theAudio.clip = sound_Fire;
anim = GetComponent<Animator>();
}
void FixedUpdate() // 정해진 초마다 (!= 프레임마다. update보다 좀 느리고 정확)
{
Spin();
SearchEnemy();
LookTarget();
Attack();
}
rateOfFire
👉 연사 속도rateOfFire
시간에 한번씩 쏜다. 이게 바로 연사 속도가 된다. 이 값이 작을 수록 연사가 같은 시간 안에 더 많이 이루어진다는 의미이므로 연사 속도가 빠름currentRateOfFire
을 프레임마다 증가시켜서 이게rateOfFire
에 도달할 때 발사하고currentRateOfFire
은 0 으로 갱신하고 다시 증가. 이를 반복
rateOfAccuracy
👉 정확도- 발사 방향을 플레이어가 정확히 위치한 방향으로부터
rateOfAccuracy
만큼 벡터의 X, Y 방향값에 더해줄 것이다.- 즉 수직, 좌우로 좀 더 튀게!
- Z 값엔 더해주지 않는다. Z 값에 더해주면 총알이 뒤로 날아갈 수 있음..
- 따라서 높을수록 정확도가 떨어진다.
- 발사 방향을 플레이어가 정확히 위치한 방향으로부터
- FixedUpdate()
- 어떤 매 시간마다
- 1️⃣ Spin() 👉 360도로 계속 뺑글뺑글 회전하다가
- 2️⃣ SearchEnemy() 👉 사정 거리내에서 타겟을 찾았다면
- 3️⃣ LookTarget() 👉 타겟을 향해 회전한다.
- 4️⃣ Attack() 👉 그리고 발사한다.
- 어떤 매 시간마다
FixedUpdate
와Update
의 차이점
- FixedUpdate
- 프레임마다 호출되지 않는다. 독립적인 타이머가 존재하여 정해진, 고정적인 시간 간격으로 호출된다.
- 프레임과 관계없이 규칙적으로 호출되므로 물리적인 연산을 할 때 이 곳에서 하는게 좋다.
- 프레임은 시스템 환경을 따라가므로 컴퓨터 환경이 좋지 않으면 느리고 불규칙적으로 변할 수 있기 때문에 Rigidbody 같은 어떤 물리 효과가 적용된 움직임 처리를 Update 안에 구현하는건 좋지 않다.
- 프레임과 관계없이 규칙적으로 호출되므로 물리적인 연산을 할 때 이 곳에서 하는게 좋다.
TimeSCale
에 의존하기 때문에Time.timeScale = 0;
이 될 때 실행되지 않는다.Time.fixedDeltaTime
마다 실행된다. 이는0.02
초로 고정되어 있다.
- 프레임마다 호출되지 않는다. 독립적인 타이머가 존재하여 정해진, 고정적인 시간 간격으로 호출된다.
- Update
- 프레임마다 호출된다.
TimeSCale
에 의존하지 않기 때문에Time.timeScale = 0;
이 될 때도 Update 함수 자체는 실행이 된다.- 다만 이 안에서
deltaTime
을 사용하여 움직임을 제어한게 있었다면 멈추겠지!
- 다만 이 안에서
Except for realtimeSinceStartup and fixedDeltaTime, timeScale affects all the time and delta time measuring variables of the Time class. If you lower timeScale it is recommended to also lower Time.fixedDeltaTime by the same amount. FixedUpdate functions will not be called when timeScale is set to zero. https://docs.unity3d.com/ScriptReference/Time-timeScale.html
✈ 평소엔 360도 회전하기
private void Spin()
{
if (!isFindTarget && !isAttack)
{
Quaternion _spin = Quaternion.Euler(0f, tf_TopGun.eulerAngles.y + (1f * spinSpeed * Time.deltaTime), 0f);
tf_TopGun.rotation = _spin;
}
}
타겟을 찾은 상태도 아니고 공격해야하는 상태도 아니라면 FixedUpdate 안에서 Y 축을 중심으로 현재 회전 값에서 spinSpeed * Time.deltaTime
만큼 더 회전한다. Z 축과 포신 방향을 일치해준 상태기 때문에 하늘을 향하는 축은 Y 축임. 따라서 Y 축을 중심으로 회전하면 된다.
✈ 타겟 찾기
private void SearchEnemy()
{
Collider[] _target = Physics.OverlapSphere(tf_TopGun.position, range, layerMask);
for (int i = 0; i < _target.Length; i++)
{
Transform _targetTf = _target[i].transform; // 이게 더 빠르다
if (_targetTf.name == "Player")
{
Vector3 _direction = (_targetTf.position - tf_TopGun.position).normalized;
float _angle = Vector3.Angle(_direction, tf_TopGun.forward);
if (_angle < viewAngle * 0.5f)
{
tf_Target = _targetTf;
isFindTarget = true;
if (_angle < 5f) // 거의 차이 안나면
isAttack = true;
else
isAttack = false;
return;
}
}
}
// 플레이어 못 찾음
tf_Target = null;
isAttack = false;
isFindTarget = false;
}
- 매번
_target[i].transform
로 접근 하는 것보다 미리_target[i].transform
을_targetTf
Transform _targetTf = _target[i].transform 이렇게 변수에 대입 시켜놓고_targetTf
를 사용하는 것이 더 빠르다. 매번_target[i].transform
로 사용하면 그 원소를 접근하는데 드는 시간도 무시할 수 없을 것이다. 배열 크기가 아무 크고 잦은 접근을 한다면.. - 사정거리
range
내에 있는 모든layerMask
레이어를 가진 충돌체들을_target
배열에 담는다._target
배열을 매번 순회해 검사하여 “Player” 플레이어가 담긴 것을 알게 된다면 👉 플레이어가 사정 거리내에 들어왔다는 뜻- 그 이후 시야각 안에 들어오는지를 체크한다.
- 플레이어와 포탑 사이각이 시야각의 절반 보다 작다면 이제 플레이어를 향해 포탑 방향을 틀어야 하므로
isFindTarget
을 True 시킨다.(LookTarget()에서 할 것) - 시야각과 5도 미만으로 차이가 나면 그냥 바로 공격 때리게 Attack()
- 그리고 리턴해버린다.
- 플레이어와 포탑 사이각이 시야각의 절반 보다 작다면 이제 플레이어를 향해 포탑 방향을 틀어야 하므로
- 그 이후 시야각 안에 들어오는지를 체크한다.
- for문을 빠져나왔다면 충돌 범위에 플레이어가 없어서 for문 안에서 return 을 못만난 것이므로 못찾은것이다.
✈ 타겟을 향해 회전하기
private void LookTarget()
{
if (isFindTarget)
{
Vector3 _direction = (tf_Target.position - tf_TopGun.position).normalized;
Quaternion _lookRotation = Quaternion.LookRotation(_direction);
Quaternion _rotation = Quaternion.Lerp(tf_TopGun.rotation, _lookRotation, 0.2f);
tf_TopGun.rotation = _rotation;
}
}
SearchEnemy() 에서 플레이어를 시야각내에서 찾았다면 isFindTarget
가 True인 상태가 된다. 즉, 타겟을 향해 이제 회전시켜야 한다. 포탑이 플레이어를 향하는 방향을 바라보게 회전해야 한다. 즉, _lookRotation
만큼 회전. Lerp 를 사용하여 부드럽게 회전시킨다.
- 플레이어쪽으로 회전이 완료되어 시야각과 플레이어와 포탑 사이의 각도가 5 도 미만으로 차이나면 SearchEnemy() 에서
isAttack = true
가 되어 공격할 수 있는 상태가 된다.
✈ 발사
private void Attack()
{
if (isAttack)
{
currentRateOfFire += Time.deltaTime;
if(currentRateOfFire >= rateOfFire)
{
currentRateOfFire = 0;
anim.SetTrigger("Fire");
theAudio.Play();
particle_MuzzleFlash.Play();
if (Physics.Raycast(tf_TopGun.position,
tf_TopGun.forward + new Vector3(Random.Range(-1, 1f) * rateOfAccuracy, Random.Range(-1, 1f) * rateOfAccuracy, 0f),
out hitInfo,
range,
layerMask)){
GameObject _HitEffect = Instantiate(go_HitEffect_Prefab, hitInfo.point, Quaternion.LookRotation(hitInfo.normal));
Destroy(_HitEffect, 1f);
if (hitInfo.transform.name == "Player")
{
hitInfo.transform.GetComponent<StatusController>().DecreaseHP(damage);
}
}
}
}
}
rateOfFire
초 간격으로 발사한다. (연사속도)- 발사 애니메이션도 재생하고 발사 효과음도 재생한다.
- 플레이어가 있는 방향으로 회전을 완료한 후 (거의 5도 미만 차이) 앞으로 (Z축. 포신과 일치하는 방향) Raycast 를 쏜다.
- Raycast 를 쏘되 좌우 수직으로 총알이 좀 튀게 한다.
foward
앞 방향으로부터 랜덤하게 [-rateOfAccuracy
,rateOfAccuracy
) 범위의 값으로 X, Y 방향으로 더 움직인 그 방향으로 쏜다.- Z 방향은.. 앞뒤로 얼마나 나가느냐의 문제니까 뒤로도 총알이 튈 수 있게 됨. 총알이 앞으로 나가는건 당연하고! 그래서 Z 방향은 보정하지 않는다.
- 맞은 상대에게 피격효과 파티클 시스템 생성
- Raycast 충돌한게 플레이어라면 HP 깎음
- Raycast 를 쏘되 좌우 수직으로 총알이 좀 튀게 한다.
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
댓글 남기기