물론 스크린좌표를 월드좌표로 변환해서 레이를 쏴서 그 오브젝트를 바라보게해도 되는데 간단하게 LookAt()을 이용해보겠다.
스크립트를 하나 만들고 RightClick으로 이름 짓는다. 스크립트를 Player에 적용한다.
Monster 인스펙터 TAG가 MONSTER로 되어 있어야 한다.
FindwidhtTag()로 간단히 찾아 monsterTR에 저장하고 LookAt()을 이용해 방향을 바꾼다.
다만. 순간적으로 바꿔지기 때문에 부드럽게 바꾸려면 다른 함수를 써야 한다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RightClick : MonoBehaviour
{
// Start is called before the first frame update
private Transform monsterTr; //컴포넌트의 캐시를 처리할 변수
void Start()
{
monsterTr = GameObject.FindWithTag("MONSTER").GetComponent<Transform>();
}
// Update is called once per frame
void Update()
{
if (Input.GetMouseButtonDown(1)) {
transform.LookAt(monsterTr);
}
}
}
다른 방법은 어차피 NavMesh가 적에 적용되어 있으니 그걸 응용해보기로 했다. 목적은 자동추적보다는 플레이어의 자동 회전에 가깝다. 일단 하이리키 Player에 NavMesh Agent를 추가한다.
교재의 프로젝트에 영향을 주지 않게 하기 위해 따로 스크립트도 추가하고 NavCtrl로 이름짓는다.
코드는 간단하다. 마우스 우클릭을 하면 Player가 자동으로 적을 따라가고 다시 우클릭을 하면 멈춘다.
다른 스크립트에도 Navigation을 멈추게 하기 위해 SetNavStop()을 추가한다.
public void SetNavStop() {
agent.isStopped = true;
}
추가된 NavCtrl 전체코드, 대부분 MonsterCtrl에서 NavMesh에 관련된 코드를 복붙했다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI; //for Navigation
public class NavCtrl : MonoBehaviour
{
// Start is called before the first frame update
private NavMeshAgent agent;
private Transform monsterTr; //컴포넌트의 캐시를 처리할 변수
void Start()
{
agent = GetComponent<NavMeshAgent>();
agent.isStopped = true; //추적을 중지
monsterTr = GameObject.FindWithTag("MONSTER").GetComponent<Transform>();
}
// Update is called once per frame
void Update()
{
if (Input.GetMouseButtonDown(1)) {
if (agent.isStopped == true) {
agent.SetDestination(monsterTr.position); //목표설정
agent.isStopped = false; //추적을 개시
} else {
agent.isStopped = true; //추적을 중지
}
}
}
public void SetNavStop() {
agent.isStopped = true;
}
}
WASD키를 눌렀을때 Navigation을 멈추기 위해 다음 코드를 PlayerCtrl스크립트 PlayerAnim() 맨윗에 추가한다.
GetComponent<NavCtrl>().SetNavStop();로 NavCtrl의 SetNavStop에 접근할 수 있다. 스크립트 이름이 class이름이고 이걸 GetComponet<T>의 인수로 바로 사용할 수 있다.
주인공 캐릭터(Player)가 쏜 총알이 몬스터에 명중하면 몬스터가 피를 흘리는 혈흔 효과(Blood Effect)를 추가해보자. 혈은 효과는 파티클로 구현하며 프로젝트뷰의 03.Prefabs/EffecExamples/Blood/Prefabs폴더에 있는 BloodSprayeffect를 사용한다. 버전의 차이인지 발견하지 못해서 아래 EffectExamples > Goop Effects > Prefabs> GoopSpray를 사용하였다.
Resources 폴더
지금까지는 프리팹을 사용하기 위해 public GameObject xxx를 선언하고 인스펙터에서 연결해놓고 사용하였다. 이번에는 스크립트에서 런모드(동적)으로 로드하는 방법을 사용한다. 이방법을 쓰기 위해서는 프리팹이나 에셋이 Resouces 폴더안에 있어야 한다.
Assets root에 Resources 폴더를 하나 만들자. BloodSprayeffect나 GoopSpray를 끌어다 놓는다. 이 Resources폴더는 빌드할때 포함되므로 필요한 것들만 놓기 바랍니다.
MonsterCtrl 스크립트에 다음과 같이 추가한다. Goopspray효과는 피튀는게 좀 작아서 localScale을 변경해 크게했다
blood.transform.localScale = new Vector3(5, 5, 5); //효과가 작아서 확대했다.
효과의 인스턴스를 만드는데 인수가 4개이다 마지막 monsterTr의 Child로 만들겠다는 뜻이다.
MonsterActionState()에서는 state가 Trace라면 agent.SetDestination(playerTr.position);로 목표물을 지정해주고, agent.isStopped = false로 추적을 시작한다.; 다른 state에 대해서도 적절한 처리를 한다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI; //for Navigation
public class MonsterCtrl : MonoBehaviour
{
//몬스트 상태
public enum State { //인스펙터뷰에 콤보박스로 표시됨
IDLE, PATROL, TRACE, ATTACK, DIE
}
public State state = State.IDLE;
public float traceDist = 10.0f;
public float attackDist = 2.0f;
public bool isDie = false;
private Transform monsterTr;
private Transform playerTr;
private NavMeshAgent agent;
public Vector3 pPos, mPos;
void Start()
{
monsterTr= GetComponent<Transform>();
playerTr = GameObject.FindWithTag("PLAYER").GetComponent<Transform>();
agent = GetComponent<NavMeshAgent>();
//agent.destination = playerTr.position;
StartCoroutine(CheckMonsterState());
StartCoroutine(MonsterAction());
}
IEnumerator CheckMonsterState() {
while(!isDie) {
pPos = playerTr.position;
mPos = monsterTr.position;
yield return new WaitForSeconds(0.3f);
//Player와 Monster간의 거리측정 스칼라값
float distance = Vector3.Distance(playerTr.position, monsterTr.position);
if(distance <= attackDist) {
state = State.ATTACK; //Attack로 상태변경
} else if(distance <= traceDist) {
state = State.TRACE; //Trace로 상태변경
} else {
state = State.IDLE; //Idle로 상태변경
}
}
}
IEnumerator MonsterAction() {
while (!isDie) {
switch(state) {
case State.IDLE:
agent.isStopped= true;
break;
case State.TRACE:
agent.SetDestination(playerTr.position);
agent.isStopped = false;
break;
case State.ATTACK:
break;
case State.DIE:
break;
}
yield return new WaitForSeconds(0.3f);
}
}
private void OnDrawGizmos() { //상태에 따라 기즈모 색깔 변경
if(state == State.TRACE) {
Gizmos.color = Color.blue;
Gizmos.DrawWireSphere(transform.position, traceDist);
}
if(state == State.ATTACK) {
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, attackDist);
}
}
}
실행하면서 Monster의 인스펙터의 MonsterCtrl 스크립트뷰를 보면 거리가 좁혀지면 IDEL이 TRACE로 바뀌면 Monster가 추적을 시작하고 더 가가워지면 ATTACK로 바뀐다. 막 도망가서 거리를 벌리면 다시 IDLE상태로 된다.
마지막으로 OnDrawGizmos()는 씬뷰에서만 보인다. 디버그용이다.
트레이스 범위에서 범위에 해당하는 파란색 구를 그려주고 공격범위에서는 범위 크기의 빨간색 구를 그려준다.
MonsterCtrl 스크립터의 코루틴 MonsterAction() 내부의 Trace와 Attack case문에 IsAttack 파라메터 부분을 넣어준다
IEnumerator MonsterAction() {
while (!isDie) {
switch(state) {
case State.IDLE:
agent.isStopped= true; //추적을 중지
anim.SetBool("IsTrace", false);
break;
case State.TRACE:
agent.SetDestination(playerTr.position); //목표설정
agent.isStopped = false; //추적을 재개
anim.SetBool("IsTrace", true);
anim.SetBool("IsAttack", false);
break;
case State.ATTACK: //공격상태
anim.SetBool("IsAttack", true);
break;
SetBool()은 파라메테명의 오자가 있으면 오류도 발생하고 호출될때 마다 해시테이블을 검색하므로 파라메터 해시값을 미리 추출해 전달하는 방식이 바람직하다. 우선 다음같이 스트링을 해시값으로 미리 바꿔 놓는다 "Hit"는 몬스터가 피격받을때 상태인데 미리 넣어두겠다
public readonly int hashTrace = Animator.StringToHash("IsTrace");
public readonly int hashAttack = Animator.StringToHash("IsAttack");
private readonly int hashHit = Animator.StringToHash("Hit");
코루팅 switch()문안의 스트링을 해시값으로 변경한다.
IEnumerator MonsterAction() {
while (!isDie) {
switch(state) {
case State.IDLE:
agent.isStopped= true; //추적을 중지
anim.SetBool(hashTrace, false);
break;
case State.TRACE:
agent.SetDestination(playerTr.position); //목표설정
agent.isStopped = false; //추적을 재개
anim.SetBool(hashTrace, true);
anim.SetBool(hashAttack, false); //공격파라메터 변경
break;
case State.ATTACK: //공격상태
anim.SetBool(hashAttack, true); //공격파라메터 변경
break;
실행해 보면 이제 몬스터가 걷고 공격한다. 이제부터는 몬스터가 피격받을때 취할 리액션을 붙여 넣겠다.
게임뷰탭옆 Ani탭을 누른후 Hit Trigger 파라메터를 하나 만든후 몬스터 모델에서 gothit을 끌어다 놓고 다음과 transition을 만든다.
Any->gothit transition은 HasExitTime 언체크, condition +를 누르고 Hit로 설정한다
gothit에서 idle 로 단방향 transition을 만든다 HasExitTime는 체크, ExitTime 0.9 isTrace false로 한다.
gothit에서 walk 로 단방향 transition을 만든다 HasExitTime는 체크, ExitTime 0.9 condition + IsTrace true, IsAttack false로 한다.
적 캐릭터가 장애물을 피해서 가는 걸 구현한다면 PathFinding같은 알고리즘이 필요하다. 유니티가 3D모델을 분석해 추적할 수 있는 내비게이션 기능을 제공하기 이전 많이 사용되었다. 유니티의 내비게이션 기능을 이용해 주인공을 추적하는 로직을 구현해 보자. 추적을 시작하면 walk애니메이션으로 변경하고 플레이어에 근접했을때 attack 애니메이션으로 변경되도록 메카님을 구성해본다.
네비게이션은 스테이즈를 구성하고 있는 3D메시(지형:Geometry)를 분석해 네비메시(NavMesh) 데이터를 미리 생성하는 방식이다. 추적할 수 있는 영역과 지나갈수 없는 영역을 미리 메시로 만들어 동작을 연출할 수 있다.
네비게이션 설정 - Navigation Static Flag
네비게이션을 만들(Bake)려면 그 대상이 무엇인지 지정해야 한다. 우선 바닥으로 사용되는 Floor의 static 옵션을 설정한다. Floor 인스펙터의 이름 오른쪽의 Static옆 삼각형을 눌러 Navigation Static만 체크한다.
Monster를 선택하고. window>AI>Navigation을 선택하면 Navigation뷰가 나오고 Bake를 누르고 마치면 파란색 영역이 만들어진다. 드럼통 주위는 파란색 메시로 채워지지 않고 구멍이 나있다. 이 영영읍 지나갈 수 없는 영억으로 인식해 추적한 대상이 장애물로 판단한다. Bake는 씬뷰에 플레이전 있는 것들만 인식한다. 동적으로 생성되는 것은 예측할수 없는 것 같다. 따라서 스테이지에 적용된 Barrel 스크립트를 제거한고 프리팹 Barrel을 여기저기 배치하고 Bake를 한번 더 누른다.
플레이어와 몬스터를 떨어트려 놓과 사이에 배치해주기 바란다. 몬스터가 피해가는지
NavMeshAgent 컴포넌트
NavMeshAgent 컴포넌트는 목표를 향해 움직일 때 서로를 피해가는 캐릭터 생성에 유용합니다. 에이전트는 내비메시를 이용하여 게임 월드에 대해 추론하고 서로 또는 기타 움직이는 장애물을 피할 방법을 이해하고 있습니다. 내비메시 에이전트의 스크립팅 API를 이용하여 경로를 찾거나 공간을 추론할 수 있습니다.
에이전트는 수직으로 서 있는 실린더에 의해 정의되며 실린더의 크기는Radius과Height프로퍼티에 의해 특정됩니다. 실린더는 오브젝트와 함께 움직이지만 오브젝트가 회전한다 해도 계속 수직으로 서 있습니다. 실린더의 모양은 다른 에이전트와 장애물간의 충돌 감지와 대응에 사용됩니다. 게임 오브젝트의 앵커 포인트가 실린더의 베이스에 없을 때 높이의 차이를 메우기 위해베이스 오프셋프로퍼티를 사용할 수 있습니다.
실린더의 높이와 반경은 실제로두 개의 다른 장소에 특정됩니다.내비메시 베이크 설정과 각 에이전트의 프로퍼티 입니다.
내비메시 베이크 설정은 내비메시 에이전트가 어떻게 정적인 월드 지오메트리와 충돌하고 또는 어떻게 회피하는지를 설명합니다. 메모리 여유량을 유지하고 CPU 로드를 지속적으로 체크하기 위해 오직 하나의 크기만이 베이크 설정에서 특정될 수 있습니다. -내비메시 에이전트 프로퍼티값은 에이전트가 움직이는 장애물 및 다른 에이전트와 어떻게 충돌하는지를 설명합니다.
대부분의 경우 두 장소 모두에 에이전트 크기를 똑같이 설정합니다. 하지만 예를 들어 크기가 큰 에이전트의 반경이 더 넓다면 다른 에이전트는 그 에이전트 주변에 공간을 더 많이 남겨둡니다. 하지만 그렇지 않다면 크기가 큰 에이전트도 마찬가지로 환경을 무시합니다.
하이라키뷰에서 Player를 선택하고 TAG에서 PLAYER를 생성하고 적용한다
Nave Mesh Agent는 인스펙터에서 추적대상을 연결할 수 없고 스크립트를 생성해야한다.
MonsterCtrl스크립트를 생성하자. Monster에 적용한다.
코드는 간단한다 다음한줄로 자동으로 추적한다. 다만 명령을 실행할 당시의 위치만이다. 명령실행후 플레이어가 움직이면 자동 갱신되지 않는다.
agent.destination = playerTr.position;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI; //for Navigation
public class MonsterCtrl : MonoBehaviour
{
private Transform monsterTr;
private Transform playerTr;
private NavMeshAgent agent;
void Start()
{
monsterTr= GetComponent<Transform>();
playerTr = GameObject.FindWithTag("PLAYER").GetComponent<Transform>();
agent = GetComponent<NavMeshAgent>();
agent.destination = playerTr.position; //목표를 플레이어로 설정
}
}
잘 된다. 처음에는 PLAYER TAG를 Monster에도 설정했다가 뺑글뺑글 도는 걸 보고 풀어줬더니 잘되었다. PLAYER TAG는 Player에만
Monster는 NavMeshAgent가 미리 베이크된 NavMesh에 붙어서 이동하기 때문에 살짝 떠 있다. 방법은 NavMesh를 Bake할대 낮춰주던지 Monster NavMeshAgent의 BaseOffset을 -0.1로 낮춰주는 거다
메카님 리타게팅을 사용하려면 적용하려는 모델과 애니메이션 클립의 Type이 둘다 Humanoid로 설정해야 한다. 따라서 monster@Gangnam Style.fbx도 Rig탭에서 타입을 Humanoid로 바꾸고 apply한다. Animation탭을 누루고 Loop Time도 체크후 Apply해 준다.
같은 Animation탭 맨밑의 프리뷰창에 05.Model/Monster폴더의 Monster를 끌어다 놓고 Play시켜보면 춤을 추는걸 확인할수 있다.
Animator 컴포넌트
Monster모델을 씬뷰또는 하이라키로 끌어다 Player옆에 놓는다.
씬뷰에 추가된 Monster는 기본적으로 Animator가 추가되어 있다. 3D모델의 애니메이션 타입을 메카님으로 설정하면 Animator컴포넌트가 기본적으로 추가된다. 앞서 제작한 Player모델은 애니메이션 타입을 레거시로 설정했기 때문에 Animation 컴포넌트가 추가된것이다. Animator는 Animation을 컨트롤 하는 컴포넌트로 레가시는 Animation컴포넌트가 그 역활을 한다.
애니메이터 컨트롤러를 사용하여 캐릭터나 오브젝트의 애니메이션 클립 세트와 관련 애니메이션 전환을 정렬하고 관리할 수 있습니다. 대부분의 경우, 여러 애니메이션을 이용하여 게임 내에서 특정 상황이 일어났을 때 에셋을 바꿔가면서 사용합니다. 예를 들어 스페이스바를 누를 때마다 걷기 애니메이션 클립에서 점프 애니메이션 클립으로 전환할 수 있습니다. 하지만 애니메이션 클립이 하나만 있을 때에도 이 클립을 게임 오브젝트에 사용하기 위해서는 애니메이터 컨트롤러에 배치해야 합니다.
애니메이터 컨트롤러는 그 안에서 사용되는 애니메이션 클립에 대한 레퍼런스를 포함하고 있으며, 애니메이션 클립과 전환의 순서도 또는 Unity 에디터 내에서 비주얼 프로그래밍 언어로 작성된 간단한 프로그램이라고 생각할 수 있는상태 머신을 사용하여 다양한 애니메이션 클립과 각 클립 사이의 전환을 관리합니다. 상태 머신에 대한 자세한 내용은여기에서 확인할 수 있습니다.
메카님 리타게팅을 사용하려면 적용하려는 모델과 애니메이션 클립의 Type이 둘다 Humanoid로 설정해야 한다. 따라서 monster@Gangnam Style.fbx도 Rig탭에서 타입을 Humanoid로 바꾸고 apply한다. Animation탭을 누루고 Loop Time도 체크후 Apply해 준다.
같은 Animation탭 맨밑의 프리뷰창에 05.Model/Monster폴더의 Monster를 끌어다 놓고 Play시켜보면 춤을 추는걸 확인할수 있다.
Animator 컴포넌트
Monster모델을 씬뷰또는 하이라키로 끌어다 Player옆에 놓는다.
씬뷰에 추가된 Monster는 기본적으로 Animator가 추가되어 있다. 3D모델의 애니메이션 타입을 메카님으로 설정하면 Animator컴포넌트가 기본적으로 추가된다. 앞서 제작한 Player모델은 애니메이션 타입을 레거시로 설정했기 때문에 Animation 컴포넌트가 추가된것이다. Animator는 Animation을 컨트롤 하는 컴포넌트로 레가시는 Animation컴포넌트가 그 역활을 한다.
애니메이터 컨트롤러를 사용하여 캐릭터나 오브젝트의 애니메이션 클립 세트와 관련 애니메이션 전환을 정렬하고 관리할 수 있습니다. 대부분의 경우, 여러 애니메이션을 이용하여 게임 내에서 특정 상황이 일어났을 때 에셋을 바꿔가면서 사용합니다. 예를 들어 스페이스바를 누를 때마다 걷기 애니메이션 클립에서 점프 애니메이션 클립으로 전환할 수 있습니다. 하지만 애니메이션 클립이 하나만 있을 때에도 이 클립을 게임 오브젝트에 사용하기 위해서는 애니메이터 컨트롤러에 배치해야 합니다.
애니메이터 컨트롤러는 그 안에서 사용되는 애니메이션 클립에 대한 레퍼런스를 포함하고 있으며, 애니메이션 클립과 전환의 순서도 또는 Unity 에디터 내에서 비주얼 프로그래밍 언어로 작성된 간단한 프로그램이라고 생각할 수 있는상태 머신을 사용하여 다양한 애니메이션 클립과 각 클립 사이의 전환을 관리합니다. 상태 머신에 대한 자세한 내용은여기에서 확인할 수 있습니다.
MuzzleFlash.png를 끌어다 MuzzleFlash에 적용한고 Shader를 Mobile/Particles/Additive로 하면 검은부분이 투명하게 바낀다.
Shader 왼쪽 삼각형을 눌러 메뉴를 펴서 Particle Texture Tiling을 0.5, 0.5로 하면 4장으로 자르게 된다.
밑의 offet을 변경해 4장중 한장을 고를 수 있다.
주인공 캐릭터 하위의 MuzzleFlash에 포한된 MeshRenderer 컴포넌트를 추출하는 로직을 FireCtrl스크립트에 추가한다. MuzzleFlash의 Offset값을 변경하려면 머터리얼 정보를 담고 있는 MeshRenderer컴포넌트에 접근해야 한다. FireCtrl에 이미 있는 firePos변수를 이용해서 접근한후 비활성화 한다. 총을 발사할때문 보이게 할것이다.
Play옆 포즈를 누르고 씬뷰탭을 선택후 ALT+좌클릭후 마우스를 움직여 씬뷰를 회전시키면 총구의 화염이 없는 것도 확인할수 있다
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
[RequireComponent(typeof (AudioSource))] //삭제되는것을 방지하는 어트리뷰트
public class FireCtrl : MonoBehaviour
{
public GameObject bullet; //총알프리팹
public Transform firePos; // 총알 발사 좌표
public AudioClip fireStx; //총소리에 사용할 음원
private new AudioSource audio; //오디오 소스 컴포넌트를 저장할 변수
private MeshRenderer muzzleFlash; //머즐이펙트 참조
// Update is called once per frame
private void Start() {
audio = GetComponent<AudioSource>(); //오디오소스를 얻어온다
muzzleFlash = firePos.GetComponentInChildren<MeshRenderer>(); //자식 컴포넌트를 얻어온다
muzzleFlash.enabled = false; //안보이게한다
}
void Update()
{ //마우스 왼쪽 버튼을 클릭했을때 Fire 함수 호출
if(Input.GetMouseButtonDown(0)) {
Fire(); //발사처리
}
}
void Fire() {
Instantiate(bullet,firePos.position, firePos.rotation); //Bullet프리팹을 동적으로 발생
audio.PlayOneShot(fireStx, 1.0f); //총소리발생
}
}
코루틴함수
유니티의 모든 스크립트는 메시지루트가 동작한다. 다양한 이벤트함수가 정해진 순서대로 실행되는 순환구조이다. 일반 함수를 호출하면 해당 함수 안의 로직이 다 수행해야만 실행이 끝난다.
함수안에서 로직이 10초 걸린다고 하면10초동안 메시지루프의 다른 로직을 기다리게 하는데 이걸 Blocking이라고 한다.
이를 개선하려면 함수를 병렬로 호출해야한다. 이걸 Multi thread라고 한다.
유니티는 코루틴이라는 병렬처리기능을 제공한다.
만일 Fade()라는 함수는 for문을 통해 알파값을 감소시켜 투명처리하는 함수인데 순식간에 처리하므로 효과를 확인할수 없어 갑자기 꺼지게 될것이다.
void Fade() {
for(float f = 1f; f>=0; f-= 0.1f) {
Color c = GetComponent<Renderer>().material.color;
c.a = f; //알파값을 천천히 감소시킨다
GetComponent<Renderer>().material.color = c;
}
}
코루틴의 아이디어는 yield return null; 키워드를 만나면 제어 권한을 유니티시스템의 메시지루프로 양보하는 방식이다.
코루틴 함수는 열거자 IEnumerator 타입으로 선언해야 하고 함수내에 하나이상의 yeild 키워드를 사용해야 한다.
Ienumerate void Fade() {
for(float f = 1f; f>=0; f-= 0.1f) {
Color c = GetComponent<Renderer>().material.color;
c.a = f; //알파값을 천천히 감소시킨다
GetComponent<Renderer>().material.color = c;
yield return null; //메시지루프로 제어권을 넘긴다.
}
}
yield return null;키워드는 함수내에 여러개 삽입될 수도 있다.
코루틴 함수는 호출시 StartCoroutine()함수를 이용하여 호출한다. 전달인자는 함수의 원형(포인터)를 권장한다. "함수명"으로 호출하는건 가비지컬렉션을 발생시키 개별적으로 정지시킬수 없기 때문이다.
StartCoroutine(Fade());
yield return null대신 yield return WaitForSecond(0.3f)같이 하면 자기 자신을 0.3초동안 멈추게 할 수 있다. 대신 로직을 잘짜 무한 루프에 안 빠지게 해야한다. 다음은 적이 죽지 않았다면 0.3초간 기다려주는 로직이다. 만일 yield return new WaitForSeconds(0.3f)가 없다면 정말 무한루프에 빠질것이다.
MuzzleFlash는 다른 게임오브젝트(FirePos)아래에 차일드화된 게임오브젝트로서, 좌표, 회전, 각도, 스케일을 수정하려면 반드시 localPosition, localRotation, localScale속성을 사용해야 한다.
MuzzleFlash를 회전시키기 위한 localRotation속성은 Quaternion 타입이므로 오일러각을 변화시키는 Quaternion.Euler(x,y,x)함수를 사용한다. 0~360도의 난수를 발생시켜 회전시켜본다. 유니티 원시모델 Quad로 만들어진 MuzzleFlash는 x축으로 -90도 회전된 모델로 z축을 기준으로 회전시켜야 하므로
다음과 같이 FireCtrl을 변경하고 실행해보면 MuzzleFlash의 텍스처 크기 회전이 불규칙하게 변하는 것을 확인할 수 있다.
각도 때문에 게임뷰에서 확인이 어렵다면 게임뷰와 씬뷰를 동시에 보면서 할수 있다. 씬뷰는 게임중에도 위치와 회전을 할 수 있다.
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
[RequireComponent(typeof (AudioSource))] //삭제되는것을 방지하는 어트리뷰트
public class FireCtrl : MonoBehaviour
{
public GameObject bullet; //총알프리팹
public Transform firePos; // 총알 발사 좌표
public AudioClip fireStx; //총소리에 사용할 음원
private new AudioSource audio; //오디오 소스 컴포넌트를 저장할 변수
private MeshRenderer muzzleFlash; //머즐이펙트 참조
// Update is called once per frame
private void Start() {
audio = GetComponent<AudioSource>(); //오디오소스를 얻어온다
muzzleFlash = firePos.GetComponentInChildren<MeshRenderer>(); //자식 컴포넌트를 얻어온다
muzzleFlash.enabled = false; //안보이게한다
}
void Update()
{ //마우스 왼쪽 버튼을 클릭했을때 Fire 함수 호출
if(Input.GetMouseButtonDown(0)) {
Fire(); //발사처리
}
}
void Fire() {
Instantiate(bullet,firePos.position, firePos.rotation); //Bullet프리팹을 동적으로 발생
audio.PlayOneShot(fireStx, 1.0f); //총소리발생\
StartCoroutine(ShowMuzzleFlash()); //총구화염효과 코루틴함수 호출
}
IEnumerator ShowMuzzleFlash() { //코루틴
Vector2 offset = new Vector2(Random.Range(0, 2), Random.Range(0, 2)) * 0.5f;
muzzleFlash.material.mainTextureOffset = offset; //4장중 한장을 선택
float angle = Random.Range(0, 360);
muzzleFlash.transform.localRotation = Quaternion.Euler(0,0, angle); //로컬회전, 쿼터니언으로 변경할것
float scale = Random.Range(1.0f, 2.0f);
muzzleFlash.transform.localScale = Vector3.one * scale; //로컬스케일변경
muzzleFlash.enabled=true; //muzzleFlash 활성화
yield return new WaitForSeconds(0.2f); // new를 붙여줘야함. 권한을 0.2초 넘겨줌
muzzleFlash.enabled=false; //muzzleFlash 비활성화
}
}
코루틴의 응용 - 임계치
게임을 실행하면 Player의 방향이 불규칙하다. 이건 마우스의 좌우 이동값이 불규칙하게 넘어오기 때문이다. Start함수를 코루틴으로 변경시켜 해결해보자. Player스크립트를 다음과 같이 수정한다. 교재는 PlayerCtrl이다.
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEditor;
using UnityEngine;
public class Player : MonoBehaviour {
// Start is called before the first frame update
Transform tr;
private float moveSpeed=10f;
private float turnSpeed=0f;
private Animation anim;
IEnumerator Start() { //start()함수는 코루틴으로 실행할 수 있다.
tr = GetComponent<Transform>();
anim = GetComponent<Animation>(); //추가된 코드
anim.Play("Idel");
yield return new WaitForSeconds(0.3f);
turnSpeed = 500f;
}
// Update is called once per frame
void Update() {
float h = Input.GetAxis("Horizontal"); //AD 입력 좌우
float v = Input.GetAxis("Vertical"); //WS 입력 전후
if (Mathf.Abs(h) > float.Epsilon || Mathf.Abs(v) > float.Epsilon) { //움직임이 없다면 불필요한 동작을 안한다.
Vector3 dir = Vector3.right * h + Vector3.forward * v;
tr.Translate(dir.normalized * moveSpeed * Time.deltaTime);
PlayerAnim(h, v);
}
float r = Input.GetAxis("Mouse X"); //마우스 x축 입력
tr.Rotate(Vector3.up * turnSpeed * Time.deltaTime * r);
}
void PlayerAnim(float h, float v) {
Debug.Log("Move");
if (v >= 0.1f) {
anim.CrossFade("RunF",0.25f);
} else if(v <= -0.1f) {
anim.CrossFade("RunB", 0.25f);
} else if(h >= 0.1f) {
anim.CrossFade("RunR", 0.25f);
} else if (h <= -0.1f) {
anim.CrossFade("RunL", 0.25f);
} else {
anim.CrossFade("Idle", 0.25f);
}
}
}
MainCamera에는 AudioListener가 기본적으로 추가되어있고 설정할 것은 없는데 카메라가 여러대일경우 하나만 남겨놓고 지워야 한다.
Resouces/Sounds폴더에서 WeaponSFX 패키지를 임포트하고 프로젝트뷰의 06.Sounds 폴더로 드래그&드롭해 위치를 옮긴다. 이패키지는 Rifle과 Shotgun 두 가지의 총소리와 폭발음이 포함돼 있다. Rifle폴더에 있는 p_m4_1사운드파일을 사용한다. 임포트한 오디오 클립은 Force To Mono옵션으 체크해서 가볍게 하고 사이즈도 줄일수 있다.
오디오 임포트 옵션 - Load Type
Decompress On Load : 로드시 압축을 해제하므로 사이즈가 큰 파일은 오버헤드를 발생시킨다. 작은 사이즈의 오디오에 적합하고 압축후에는 CPU자원을 덜 소비한다.
Compressed in Memory : 압축된 상태로 메모리에 상주한다. 큰 사이즈의 오디오에 적합하다.
Streaming : HDD에서 부터 스트리밍 하듯이 재생한다. 메모리가 필요없다.
오디오 임포트 옵션 - Compression Format
PCM : 비압축
ADPCM : 압축율 3.5배로 노이즈가 포함돼어 있는 음원에 적합
Voris / MP3 : 70%정도의 압축률
총소리 구현
p_mp_1.wav을 선택해 인스펙터에서 다음과 같이 설정한다. 총소리는 발사할 때마다 총소리를 발생시키므로 Decompress On Load로 Compression Format은 ADPCM정도로 한다.
Bullet프리팹을 선택해 AudioSoruce컴포넌트를 추가한다. 속성은
Audio Clip : p_mp_1.wav , 발생시킬 음원 파일
Play On Awake : 체크, 해당 컴포넌트가 활성화될 때 자동 재생 여부
Min Distance : 5 , 볼륨 100% 값으로 음원이 들리는 영역범위
Max Distance : 10 , 음원이 들리는 최대범위
하이라키의 Bullet을 수정했기 때문에 Override>Apply All버튼을 클릭해 원본 Bullet프리팹에 저장한다. 다른 방법은 Audio Source 컴포넌트에서 우클릭후 팝업된 메뉴에서 Added Component>Apply to Prefab "Bullet"을 선택한는 것이다.
이렇게 Bullet Prefab에서 구현한 총소리는 충돌하자 마자 삭제되므로 사운드가 끊어지는 현상이 발생한다. 따라서 스크립트로 처리해 보겠다. Bullet Prefab에서 AudioSource 컴포넌트를 제거한다.
하이라키뷰의 Player를 선택하고 AudioSource컴포넌트를 추가한다. 수정할 필요는 없다. 다음과 같이 스크립트를 추가하고 스크립트에 오디오클립을 연결해준다.
FireCtrl 스크립 코드
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
[RequireComponent(typeof (AudioSource))] //삭제되는것을 방지하는 어트리뷰트
public class FireCtrl : MonoBehaviour
{
public GameObject bullet; //총알프리팹
public Transform firePos; // 총알 발사 좌표
public AudioClip fireStx; //총소리에 사용할 음원
private new AudioSource audio; //오디오 소스 컴포넌트를 저장할 변수
// Update is called once per frame
private void Start() {
audio = GetComponent<AudioSource>();
}
void Update()
{ //마우스 왼쪽 버튼을 클릭했을때 Fire 함수 호출
if(Input.GetMouseButtonDown(0)) {
fire();
}
}
void fire() {
Instantiate(bullet,firePos.position, firePos.rotation); //Bullet프리팹을 동적으로 발생
audio.PlayOneShot(fireStx, 1.0f); //총소리발생
}
}
여러개의 드럼통의 같은 텍스처로 적용되여 있다. 시작과 동시에 다양하게 바꿔보자. 텍스처의 적용은 Mesh Rendere 컴포넌트에 연결된 Material에서 지정한다. 여기에 사용한 Barrel모델의 Mesh Renderer컴포넌트는 Barrel 하위에 있는 Barrel에 적용되어 있다. 부모 오브젝트는 빈 오브젝트이다.
하위의 Mesh Renderer를 연결할 변수를 선언하고 연결후 사용할 수도 있지만 BarrelCtrl스크립트에서 동적으로연결해 보자. 추가된 코드는 다음과 같다.
public Texture[] textures; //무작위로 적용할 텍스쳐배열
private new MeshRenderer renderer; //하위 MeshRenderer를 저장할 변수, new를 사용하는데 Component.renderer로 정의된 멤버 변수로서 new키워드를 사용해야 한다는데 잘 이해가 안 간다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BarrelCtrl : MonoBehaviour {
public GameObject expEffect; //폭발효과를 연결할 참조
public Texture[] textures; //무작위로 적용할 텍스쳐배열
public float radius = 10.0f; // 검사할 주변 반경을 설정하였고
private new MeshRenderer renderer; //하위 MeshRenderer를 저장할 변수
private Transform tr;//컴포넌트를 저장할 변수
private Rigidbody rb;
private int hitCount = 0; //총알맞은 회수를 누적시킬 변수
void Start() {
tr = GetComponent<Transform>();
rb = GetComponent<Rigidbody>();
renderer= GetComponentInChildren<MeshRenderer>(); //자식컴포넌트 추출
int idx = Random.Range(0, textures.Length);//난수발생
renderer.material.mainTexture= textures[idx]; //텍스처지정
}
private void OnCollisionEnter(Collision col) {
if (col.collider.CompareTag("BULLET")) {
if (++hitCount == 3) {
ExpBarrel();
}
}
}
void ExpBarrel() { //드럼통을 폭발시킨다
// 파티클효과을 참조
GameObject exp = Instantiate(expEffect, tr.position, Quaternion.identity);
Destroy(exp, 5.0f); //3초후 파티클 효과 제거
//rb.mass = 1.0f; //mass를 20에서 1로 가볍게 한다.
//rb.AddForce(Vector3.up * 1500.0f); //위쪽으로 날라가게 힘을 준다
IndirectDamage(tr.position); //파괴력을 전달
Destroy(gameObject, 3.0f); //3초후 드럼통 제거
}
void IndirectDamage(Vector3 pos) {
// 주변의 드럼통 추출
Collider[] cols = Physics.OverlapSphere(pos, radius, 1 << 3); //주변객체를 검색해서 리턴
foreach (var col in cols) { //차례대로 반복
rb = col.GetComponent<Rigidbody>();
rb.mass = 1.0f; //무게를 가볍게하고
rb.constraints = RigidbodyConstraints.None; // 움직임과 회전의 제한을 푼다
rb.AddExplosionForce(1500.0f, pos, radius, 1200.0f); //반경안에서 폭발력을 생성
}
}
}
OverlapSphereNonAlloc
Physics.OverlapSphere함수는 실행시 Sphere범위에 검출될 개수가 명확지 않을때만 사용해야한다. 동적으로 메모리를 만들기 때문에 메모리 Garbage가 발생하기 때문이다. 따라서 Sphere범위에 검출될 개수가 명확할 때는 Garbage가 발생하지 않는 Physics.OverlapSphereNoneAlloc함수를 사용하기를 권장한다. 이 함수는 결괏값을 저장할 정적 배열을 미리 선언하여 실행정에 배열의 크기를 변경할 수 없다. 우리는 배럴을 20개 배치했기 때문에 최대 검출치가 20개 이므로 이 함수를 사용할수 있는 것이다.