전체 페이지뷰

2017년 8월 2일 수요일

Unity Tutorial: Survival Shooter, part 4

Harming Enemies



플레이어 캐릭터는 이미 총을 기본으로 내장하고 있었던걸 다 아실 겁니다. 이제 플레이어가 적을 공격할 수단을 만들어 보겠습니다. 그러기 위해서 적에게 먼저 HP를 부여해야 합니다.

스크립트는 이미 Assets>Scripts>Enemy 폴더에 EnemyHealth라는 이름으로 만들어져 있습니다. 이 스크립트를 Zombunny에 연결합니다. 일단 프로퍼티에 Death ClipZomBunny Death로 정해주고 에디터로 열어봅시다.



처음 코드 내용은 PlayerHealth와 상당히 유사합니다. 좀 다른 것은 sinkSpeed, isSinking 같은 것들인데 이것은 적이 쓰러지고 나서 서서히 가라앉다가 사라져야 하는데, 그것에 관련된 것들입니다.

변수 선언이 끝났으면 Awake()에서 레퍼런스들을 연결하고 초기화를 거치는데 처음 보는것이 나왔습니다.
void Awake ()
{
    anim = GetComponent <Animator> ();
    enemyAudio = GetComponent <AudioSource> ();
    hitParticles = GetComponentInChildren <ParticleSystem> ();
    capsuleCollider = GetComponent <CapsuleCollider> ();
    currentHealth = startingHealth;
}
cs

GetComponentInChildren<>이 그것인데, Zombunny의 모델에 자식으로 들어있는 총맞는 효과를 얻어오는 것입니다.

그리고 Update()에서 Enemy의 sinking 효과를 구현합니다.

void Update ()
{
    if(isSinking)
    {
        transform.Translate (-Vector3.up * sinkSpeed * Time.deltaTime);
    }
}
cs
초당 sinkSpeed의 속도로 서서히 아래로 가라앉도록 하는 코드입니다.

다음으로 TakeDamage라는 public 메소드가 등장합니다.

public void TakeDamage (int amount, Vector3 hitPoint)
{
    if(isDead)
        return;
    enemyAudio.Play ();
    currentHealth -= amount;
            
    hitParticles.transform.position = hitPoint;
    hitParticles.Play();
    if(currentHealth <= 0)
    {
        Death ();
    }
}
cs

이미 죽었다면 더 이상 데미지를 줄 필요가 없으므로 그것은 판정하는 코드를 먼저 거칩니다.  그리고 총을 맞았을 때 내는 소리를 플레이하고, 현재 HP를 정해진만큼 깎습니다. 총맞은 효과는 맞은 자리에서 나야하므로 인수로 받은 히트포인트로 hitParticles의 위치를 옮겨서 에니메이션을 플레이하고, HP가 0 이하로 떨어졌다면 죽도록 만드는 Death() 메소드를 호출하는 코드가 되겠습니다.

void Death ()
{
    isDead = true;
    capsuleCollider.isTrigger = true;
    anim.SetTrigger ("Dead");
    enemyAudio.clip = deathClip;
    enemyAudio.Play ();
}
cs

Death() 함수는 별 어려울 것이 없습니다. 중간에 캡슐 콜라이더를 트리거로 바꾸는 이유는 죽은 다음에는 더 이상 장애물로 작용하지 않고 통과될 수 있도록 하기 위한 것입니다.

다음으로 StartSinking()이라는 퍼블릭 메소드가 등장합니다.

public void StartSinking ()
{
    GetComponent <UnityEngine.AI.NavMeshAgent> ().enabled = false;
    GetComponent <Rigidbody> ().isKinematic = true;
    isSinking = true;
    //ScoreManager.score += scoreValue;
    Destroy (gameObject, 2f);
}
cs

죽어서 쓰러지고 난 후이므로 쫓아다니는데 사용했던 NavMeshAgent는 false로 바꾸고, 더 이상 물리법칙이 작용치 않도록 IsKinematic을 true로 설정합니다. 그리고 IsSinking을 true로 바꾸어주고 2초가 지나면 오브젝트가 제거되도록 합니다.

StartSinking은 우리가 직접 호출하는 것이 아니라 에니메이션 이벤트에서 호출되어야 합니다. 우리의 모든 적들은 Death라는 에니메이션 이벤트를 가지고 있습니다. 우리가 할 일은 이 이벤트 상의 어느 시점에서 StartSinking이라는 함수를 사용해야 한다고 지정해주는 것 뿐입니다.

Assets>Models>Characters 폴더의 Zombunny를 선택하고 인스펙터를 봅시다. 클립들 중 Death를 선택하고 아래 Events를 펼쳐보면 이미 StartSinking이 이벤트 함수로 선택되어 있는 것이 보입니다(원래는 직접 해야 하는 것이지만 튜토리얼이기 때문에 미리 되어 있는 것입니다).



EnemyHealth가 생겼으니 이제 EnemyAttack에서 주석처리 했던 부분을 해제할 수 있게 되었습니다. EnemyAttack 스크립트를 열어봅시다.

EnemyHealth와 관련하여 세 군데의 주석처리된 곳이 있었습니다. 주석을 모두 제거합니다.

스크립트를 저장하고 다시 유니티로 돌아옵니다. 이제 적은 잠시 잊고 플레이어가 총을 발사하는 것을 구현할 차례가 되었습니다. 전에 말했다시피 이미 총에 대한 메쉬가 플레이어에 들어 있습니다. 다만 이 총이 원하는 바 대로 작동하게 하려면 몇 가지 추가해줘야할 것이 있습니다.

첫 번째로 추가할 것은 총이 불을 뿜도록 하는 Particle component입니다. 그리고 나서 발사되는 소리를 추가하고, 총이 발사되었을 때 볼 수 있도록 조명을 추가할 것입니다. 마지막으로 총탄이나 레이저가 발사되는 것처럼 보이도록 하는 line renderer를 추가할 것입니다.

Assets>Prefabs 폴더에 가면 GunParticle이라는 프리팹이 있습니다. 이 오브젝트 전체가 필요한 것이 아니므로 프리팹을 드래그하여 붙이는 대신 이번에는 인스펙터창에서 Particle System 옆 톱니 표시를 누르고 Copy Component를 선택해서 복사합니다.


복사가 되었으면 이번엔 Hierarchy창의 Player>GunBarrelEnd를 선택하고 Transform옆의 톱니를 눌러 PasteComponent As New를 선택합니다. 그렇게 하면 총구 끝에 복사된 Component가 추가됩니다.


총 발사에 대한 스크립트는 나중에 추가하기로 하고 다음으로는 line renderer를 추가할 것입니다. 여전히 GunBarrelEnd가 선택된 채로 Add Component>Effects>Line Renderer를 선택합니다. 씬 뷰에서 보면 추가된 렌더러는 거대한 보라색 블럭처럼 보입니다. 조절이 필요하겠습니다.

인스펙터 창에서 Materials 프로퍼티를 펼치고 Element 0 옆의 톱니를 눌러 LineRendererMaterial을 추가합니다. 다음으로 Width0.05로 조절합니다(영상에는 Start, End 두 개의 Width가 있지만 현재는 한개뿐입니다). 그리고 Line Renderer 옆의 체크박스를 해제하여 꺼 둡니다(총은 발사 버튼이 눌러졌을 때에만 발사되기 때문에 평소에는 꺼져 있는 것이 맞습니다).



다음으로 총이 발사될 때 비춰줄 라이트를 Add Component>Rendering>Light 하여 추가합니다. Color 프로퍼티는 라인 렌더러와 유사한 노란색으로 조절하고 마찬가지로 총이 쏘아졌을 때에만 켤 수 있도록 일단 체크 해제합니다.



다음은 총소리 차례입니다. Add Component>Audio>Audio Source를 선택해서 컴포넌트를 추가합니다. 그리고 Audio ClipPlayer Gunshot으로 하고, Play On Awake는 체크 해제 합니다.



이것으로 총의 준비가 끝났습니다. 마지막 남은 퍼즐은 플레이어가 실제로 총을 쏘고 적에게 데미지를 입힐 수 있게 스크립트를 만드는 것입니다. 역시나 PlayerShooting이라는 이름으로 이미 스크립트가 작성되어 있습니다. 드래그하여 GunBarrelEnd에 연결하고(Player가 아닙니다) 에디터로 열어봅시다.

언제나처럼 몇 개의 퍼블릭, 프라이빗 변수 선언으로 시작합니다.

public int damagePerShot = 20;
public float timeBetweenBullets = 0.15f;
public float range = 100f;
float timer;
Ray shootRay = new Ray();
RaycastHit shootHit;
int shootableMask;
ParticleSystem gunParticles;
LineRenderer gunLine;
AudioSource gunAudio;
Light gunLight;
float effectsDisplayTime = 0.2f;
cs

한발당 데미지, 한발 사이 시간 간격, 사거리에 해당하는 퍼블릭 변수를 선언하고, 각종 레퍼런스와 타이머, 발사효과 지속시간 등을 프라이빗으로 선언했습니다. Ray는 총이 발사되는 길을 위한 것이고, RaycastHit은 이 Ray가 어디와 부딪히는가를 알기 위한 것입니다. shootableMask는 Shootable 레이어에 해당하는 것만 때릴 수 있도록 하기 위한 레이어마스크입니다.

이것들을 Awake()에서 초기화합니다.

void Awake ()
{
    shootableMask = LayerMask.GetMask ("Shootable");
    gunParticles = GetComponent<ParticleSystem> ();
    gunLine = GetComponent <LineRenderer> ();
    gunAudio = GetComponent<AudioSource> ();
    gunLight = GetComponent<Light> ();
}
cs

Update()에서는 타이머를 계산하고 발사 버튼이 눌려졌는지와 발사이펙트 지속 시간이 되었는지를 판정합니다.

void Update ()
{
    timer += Time.deltaTime;
    if(Input.GetButton ("Fire1"&& timer >= timeBetweenBullets && Time.timeScale != 0)
    {
        Shoot ();
    }
    if(timer >= timeBetweenBullets * effectsDisplayTime)
    {
        DisableEffects ();
    }
}
cs

DisableEffects()는 퍼블릭 메소드입니다. 발사 시 활성화되었던 gunLine과 gunLight를 다시 꺼주는 역할을 하는데 이것이 퍼블릭인 이유는 A)다른 스크립트에서 참조 가능하게, B)다른 게임오브젝트의 스크립트에서 참조 가능하게, C) 에니메이션 이벤트 등에서 접근가능하게 하기 위함입니다.

Shoot()은 이 스크립트의 핵심 함수입니다.

void Shoot ()
{
    timer = 0f;
    gunAudio.Play ();
    gunLight.enabled = true;
    gunParticles.Stop ();
    gunParticles.Play ();
    gunLine.enabled = true;
    gunLine.SetPosition (0, transform.position);
    shootRay.origin = transform.position;
    shootRay.direction = transform.forward;
    if(Physics.Raycast (shootRay, out shootHit, range, shootableMask))
    {
        EnemyHealth enemyHealth = shootHit.collider.GetComponent <EnemyHealth> ();
        if(enemyHealth != null)
        {
            enemyHealth.TakeDamage (damagePerShot, shootHit.point);
        }
        gunLine.SetPosition (1, shootHit.point);
    }
    else
    {
        gunLine.SetPosition (1, shootRay.origin + shootRay.direction * range);
    }
}
cs

타이머를 리셋하고 오디오, 라이트 발사효과를 활성화합니다. 발사효과는 앞의 효과가 끝나지 않았을 경우에 대비하여 먼저 멈추고 다시 플레이합니다. 다음으로 라인 렌더러를 활성화 하고 라인의 출발점인 0을 현재의 GunBarrelEnd 위치에 맞춥니다.

선이 형성되려면 두개의 점이 필요합니다. 다른 점은 어떻게 정해질까요?

그것을 결정하기 위해 shootRay를 생성합니다. shootRayorigin은 현재의 transform 오리진 위치에 맞추고 directionforward로 정했습니다. 이 forward라는 것은 z축의 기준선을 뜻합니다.


forward로 100유닛까지의 선을 뻗어 부딪힌 콜라이더가 있으면 그 콜라이더의 EnemyHealth를 얻어옵니다. 이 enemyHealth가 null이 아니면 적이고 null이면 배경 장애물입니다. 따라서 null이 아닐 때에만 데미지를 주고 그 콜라이더까지 라인
렌더러를 뻗습니다. shootRay와 부딪히는 것이 없으면 사정거리까지 발사시켜주고 끝냅니다.

스크립트를 저장하고 유니티로 돌아옵니다. 플레이어 오브젝트에 많은 것들이 적용되었습니다. 따라서 우리가 미리 저장했던 프리팹과는 상당히 달라졌는데 이것들을 기존의 프리팹에 적용하려면 Hierarchy에서 Player를 선택하고 상단 Prefab 탭에서 Apply를 누릅니다.


씬을 저장하고 테스트 해봅시다.

총소리가 크군요. 그런데 적을 죽이고 나니 SetDestination 관련 에러가 뜹니다. Window>Console을 선택하여 로그를 보니 EnemyMovement에서 발생했습니다. EnemyHealth가 완성되었는데 EnemyMovement 스크립트에서 관련 코드를 주석처리 해놓은 곳이 있습니다. 그것때문이니 주석을 해제합니다.

그리고 PlayerHealth 스크립트에 PlayerShooting 관련 주석도 해제합시다. 이제 플레이어가 총을 쏘고 적을 죽이거나 적의 공격을 받아 자신이 쓰러질 수 있게 되었습니다.



Scoring points



적을 쓰러뜨렸지만 성과가 보이질 않습니다. 점수를 계산하고 UI로 화면에 표현해야 할 필요가 있습니다. Hierarchy 창으로 가서 HUDCanvas를 다시 봅시다. 2D 모드를 눌러 화면을 전환하고 HUDCanvas를 더블클릭하여 보기 좋게 합니다. 그리고 HUDCanvas에 우클릭하고 UI>Text를 선택하여 ScoreText라는 이름으로 추가합니다.


지금 이 텍스트는 캔버스의 중간에 자리잡고 있습니다. ScoreText의 Rect Transform에서 앵커 프리셋을 활성화한 후 Alt키나 Shift키를 누르지 않은 상태에서 Top, Center를 선택해서 앵커만 상단으로 옮깁니다. 그 후 Pos Y를 -55로 바꿔서 앵커에 상대적으로 하방에 자리잡게 합니다. Width는 300, Height는 50으로 하겠습니다.


그리고 몇 가지 프로퍼티들을 더 조절하여 어울리게 합니다.


다음으로 글씨에 그림자 효과를 주는 컴포넌트를 추가하겠습니다. Add Component>UI>Effects>Shadow를 선택하고 Effect Distance를 2, -2로 바꿔주면 됩니다.


이제 이 텍스트를 관리하는 스크립트를 추가하겠습니다.
Assets>Scripts>Managers폴더에 ScoreManager라는 스크립트가 있습니다. 드래그하여 Hierarchy의 ScoreText에 연결하고 에디터로 엽니다.

using UnityEngine;
using UnityEngine.UI;
using System.Collections;
 
public class ScoreManager : MonoBehaviour
{
    public static int score;
 
 
    Text text;
 
 
    void Awake ()
    {
        text = GetComponent <Text> ();
        score = 0;
    }
 
 
    void Update ()
    {
        text.text = "Score: " + score;
    }
}
cs

코드는 매우 간단합니다. score라는 변수를 static으로 지정하여 게임전체에서 공통의 점수로서 사용할 수 있게 하고, Awake()에서 score와 텍스트에 대한 레퍼런스를 초기화합니다. Update()에서는 바뀐 score를 반영해서 텍스트를 바꿔주기만 하면 됩니다.

이 score는 EnemyHealth 스크립트에서 사용합니다. 적이 죽어서 가라앉기 시작하는 StartSinking()내에 score에 점수를 더해주는 코드가 주석처리되어 있습니다. 해제하고 스크립트를 저장합니다.

유니티로 돌아가 씬을 저장하고 플레이해 봅니다. 적을 쓰러뜨리니 점수가 10점 올라갔습니다. Zombunny가 이것으로 완성된듯 싶습니다. 이제는 이 Zombunny를 프리팹으로 만들겁니다. 그래야 여러 개를 스포닝할 때 사용할수가 있습니다.

Hierarchy의 Zombunny를 드래그하여 Assets>Prefabs 폴더에 가져다 놓아 프리팹화하고, Hierarchy에서는 삭제합니다.

댓글 없음:

댓글 쓰기