[Unity] Space-Shooter (2)

suhan0304·2024년 6월 18일

유니티 - SpaceShooter

목록 보기
2/6
post-thumbnail

Assets

https://assetstore.unity.com/packages/vfx/particles/particle-pack-127325


Updates

Bullet 구현

  • SparkEffect

Barrel 구현

  • SparkEffect
  • ExpEffect
  • MaaauzzleFlash

Points

Rigidbody

컴포넌트의 각 속성들이 어떤 역할인지만 잘 알고 있자.

충돌 감지

  • 충돌 발생 (Collision Event) 조건

  • Trigger Event 발생 조건

CompareTag 활용할 것

게임 오브젝트의 문자 속성을 가져오는 코드 gameObject.tag는 해당 문자열의 복사본을 생성해서 메모리에 할당한다. 즉, 가비지 컬렉션(Garbage Collection)의 대상이 된다. 가비지 컬렉션은 메모리에 자동으로 할당된 저장 공간을 일정 시간이 지난 후 사용하지 않으면 자동으로 해제하는 동작을 말한다. 따라서 가비지 컬렌션 처리해야 할 대상이 많아질수록 끊김 현상이 발생한다. 그렇기 때문에 CompareTag 함수를 사용해서 가비지 컬렉션의 발생을 방지하자.

Gizmos

using UnityEngine;

public class MyGizmos : MonoBehaviour
{
    public Color _color = Color.yellow;
    public float _radius = 0.1f;

    void  OnDrawGizmos() {
        Gizmos.color = _color;
        Gizmos.DrawSphere(transform.position, _radius);
    }
}

위와 같은 스크립트를 생성해서 오브젝트 컴포넌트에 추가해줌으로서 해당 오브젝트에 기즈모를 그릴 수 있다.

궤적 효과 (Trail Renderer)

오브젝트를 따라가는 궤적 효과를 구현하는 방식은 여러가지 있지만 대부분 오브젝트가 이동함과 동시에 동적으로 메시를 만들고 일정 시간이 흐른 다음 생성된 메시를 제거해 나가는 방식이 흔히 접할 수 있는 기법이다.

유니티에서는 Trail Renderer 컴포넌트로 메시의 동적 생성을 쉽게 처리해준다.

오브젝트를 움직이니 Train Renderer로 인해 메시가 동적으로 생성된 모습을 확인할 수 있다. Material을 적절히 수정해줘서 아래와 같은 효과를 구현할 수 있다.

Min Vertex Distance : 버텍스 간의 최솟값을 설정하는 것으로 값이 작을수록 오밀조밀한 폴리곤을 생성한다. 물체가 직선으로만 이동한다면 값을 크게해서 동적으로 생성되는 폴리곤의 값을 줄이고, 포물선 운동처럼 곡선 이동한다면 값을 작세 수정해 부드러운 궤적으로 표현될 수 있게 적절히 조절하자.

색상과 투명도를 좀 손봐주면 아래와 같이 궤적 효과를 지닌 총알 구현이 완료된다.

충돌 지점과 법선 벡터

충돌 지점의 정보를 가져오기 위해서는 GetContact (모든 지점의 정보가 필요하다면 GetContacts 함수)를 사용한다.

  • public ContatcPoint GetContatc(int index)
  • public int GetContact(ContactPoint[] contacts)

GetContactPoint 함수의 반환 타입은 ContatcPoint 구조체 타입으로 아래와 같이 정의돼 있다.

위의 CompareTag와 비슷한데 충돌 지점의 접점 정보는 Collision.Contacts 속성으로도 알 수 있지만 contacts 속성은 가비지 컬렉션을 발생시킨다. 따라서 GetContact, GetContacts를 권장한다.

폭발 영향 주기

일단 폭발을 영향을 줄 사물들끼리 같은 레이어에 위치시킨다. (여기서는 BARREL 레이어에 모두 위치시킴) 그 다음에 Physics.OverlapSphere로 겹치는 오브젝트 콜라이더를 모두 가져온 다음에 폭발 영향을 준다.

void IndirectDamage(Vector3 pos) {
    Collider[] colls = Physics.OverlapSphere(pos, radius, 1 << 3);
    
    foreach (Collider coll in colls) {
	// 폭발 범위에 포함된 드럼통의 Rigidbody 컴포너트 추출
	rb = coll.GetComponent<Rigidbody>();
	// 드럼통의 무게를 가볍게
	rb.mass = 1.0f;
	// FreezeRotation 제한값을 해제
	rb.constraints = RigidbodyConstraints.None;
	// 폭발력을 전달
	rb.AddExplosionForce(1500.0f, pos, radius, 1200.0f);
}
  • 1<<3으로 표현? : 3번째 인자 레이어는 비트연산 표기법을 사용해서 3번째 레이어를 의미하는 1<<3으로 표현, 물론 8(2^3)으로 표현해도 되지만 굳이 10진수를 사용하지 않고 비트 연산을 표기하는 이유는 간편하게 다양한 논리 연산이 가능하기 때문이다.

  • Rigidbody.AddExpolisionForce(횡 폭발력, 발 원점, 폭발 반경, 종 폭발력);

OverlapSphereNonAlloc : 원래 OverlapSphere는 검출될 개수가 명확하지 않을 때만 사용해야 한다. 메모리 Garbage가 발생하기 때문인데 따라서 개수가 명확할 때는 Garbage가 발생하지 않는 OverlapSphereNonAlloc을 사용하자.

Collider[] colls = new Collider[10];
void IndirectDamage(Vector3 pos) {
    //Collider[] colls = Physics.OverlapSphere(pos, radius, 1 << 3); //Garbage Collection
    
    Physics.OverlapSphereNonAlloc(pos, radius, colls, 1 << 3);
}

오디오

오디오는 기본적으로 AudioListener, AudioSource 컴포넌트가 필요하다. 기본적으로 메인 카메라에 Listener가 들어있다.

사우드를 스테레오, 모노 어떤 것을 사용할지 잘 결정하자. 모바일 게임에서 스테레오 사운드는 용량과 성능 저하의 원인이 될 수 있다. 따라서 스테레오 음원으로 음향 효과를 극대화 할 것이 아니라면 모노 음원으로 변환하여 사용하는 것이 성능면에서 유리한다.

이 외에도 로드 옵션, 임포트 옵션은 대부분 크게 문제가 없지만 잘못 설정하면 성능 저하의 원인이 되므로 적절하게 설정하자.

Preload Audio Data : 씬이 로딩될 때 씬에서 사용하는 모든 오디오 클립이 미리 로딩 (기본값은 체크), 언체크 시 처음 오디오를 재생할 때 로딩되기 때문에 레이턴시가 발생

Import Settings

  • 배경음악과 환경음 : 대부분 큰 파일은 실행할 때 미리 압축을 풀어서 메모리에 보관하면 가용 메모리가 줄어든다. 따라서 아래 사진과 같은 임포트 설정을 권한다. 특히 Compressed In Memory를 사용할 경우 압축해 메모리에 올리는 것으로 품질을 떨어뜨려 파일 크기를 줄인다. 귀로 들어도 음질 손상이 없는 범위 내에서 적절하게 Quality 속성값을 설정한다.

  • 상황별 임포트 : 오디오의 발생 빈도나 크기에 따라 아래와 같은 임포트 설정을 권한다.

AudioSource

3D Sound Settings의 Min, Max Distance를 설정해주면 거리에 따른 소리 출력이 가능하다. Min Distance 내에서는 100%의 불륨 크기로, Max Distance의 경우에는 50%로 점차 작아지는 볼륨 크기로 들린다.

[RequireComponent(typeof(AudioSource))]

RequireComponent : 스크립트에서 반드시 함께 있어야만 하는 컴포넌트를 자동으로 추가하고, 실수로 의존성 있는 컴포넌트를 삭제하는 것을 방지하기 위한 어트리뷰트

private AudioClip fireSfx;

private new AudioSource audio;
    void Start()
{
    audio = GetComponent<AudioSource>();
}

void Fire() {
    Instantiate(bullet, firePos.position, firePos.rotation);

    audio.PlayOneShot(fireSfx, 1.0f);
}
void Fire() {
    Instantiate(bullet, firePos.position, firePos.rotation);

    audio.PlayOneShot(fireSfx, 1.0f);
}

코루틴 호출 방식

  • StartCoroutine(a()) : 함수의 원형을 전달하는 방식 (권장)
  • StartCoroutine("a") : 함수명을 문자열로 전달하는 방식

계속해서 말하지만 문자열로 뭔가를 사용할 때는 거의 웬만하면 가비지 컬렉션이 발생한다. 함수의 원형을 넘기는 방식을 사용하도록 하자.


Scripts

PlayerCtrl.cs

IEnumerator Start()
{
    tr = GetComponent<Transform>();
    anim = GetComponent<Animation>();

    anim.Play("Idle");

    turnSpeed = 0.0f;
    yield return new WaitForSeconds(0.3f);
    turnSpeed = 80.0f;
}

FireCtrl.cs

[RequireComponent(typeof(AudioSource))]
public class FireCtrl : MonoBehaviour
{
    public GameObject bullet;
    public Transform firePos;
    public AudioClip fireSfx;

    private new AudioSource audio;
    private MeshRenderer muzzleFlash;

    void Start()
    {
        audio = GetComponent<AudioSource>();

        muzzleFlash = firePos.GetComponentInChildren<MeshRenderer>();

        muzzleFlash.enabled = false;
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButtonDown(0)) {
            Fire();
        }
    }

    void Fire() {
        Instantiate(bullet, firePos.position, firePos.rotation);

        audio.PlayOneShot(fireSfx, 1.0f);

        StartCoroutine(ShowMuzzleFlash());
    }

    IEnumerator ShowMuzzleFlash() {
        Vector2 offset = new Vector2(Random.Range(0, 2), Random.Range(0,2)) * 0.5f;
        muzzleFlash.material.mainTextureOffset = offset;

        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;

        yield return new WaitForSeconds(0.2f);

        muzzleFlash.enabled = false;
    }
}

RemoveBullet.cs

public class RemoveBullet : MonoBehaviour
{
    public GameObject sparkEffect;
    void OnCollisionEnter(Collision coll) {
         if (coll.collider.CompareTag("BULLET")) {
            ContactPoint cp = coll.GetContact(0);

            Quaternion rot = Quaternion.LookRotation(-cp.normal);

            GameObject spark = Instantiate(sparkEffect, cp.point, rot);

            Destroy(spark, 0.5f);

            Destroy(coll.gameObject);
         }       
    }
}

BulletCtrl.cs

public class BulletCtrl : MonoBehaviour
{
    public float damage = 20.0f;

    public float force = 1500.0f;

    private Rigidbody rb;

    void Start() {
        rb = GetComponent<Rigidbody>();

        rb.AddForce(transform.forward * force);
    }
}

BarrelCtrl.cs

public class BarrelCtrl : MonoBehaviour
{
    public GameObject expEffect;

    public Texture[] textures;
    public float radius = 10.0f;
    private new MeshRenderer renderer;

    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];
    }

    void OnCollisionEnter(Collision coll) {
        if (coll.collider.CompareTag("BULLET")) {
            if (++hitCount == 3) {
                ExpBarrel();
            }
        }
    }

    void ExpBarrel() {
        GameObject exp = Instantiate(expEffect, tr.position, Quaternion.identity);
        Destroy(exp, 5.0f);

        //rb.mass = 1.0f;
        //rb.AddForce(Vector3.up * 1500.0f);

        IndirectDamage(tr.position);

        Destroy(gameObject, 3.0f);
    }

    //Collider[] colls = new Collider[10];
    void IndirectDamage(Vector3 pos) {
        Collider[] colls = Physics.OverlapSphere(pos, radius, 1 << 3); //Garbage Collection
        
        //Physics.OverlapSphereNonAlloc(pos, radius, colls, 1 << 3);

        foreach (Collider coll in colls) {
            rb = coll.GetComponent<Rigidbody>();

            rb.mass = 1.0f;

            rb.constraints = RigidbodyConstraints.None;

            rb.AddExplosionForce(1500.0f, pos, radius, 1200.0f);
        }
    }
}
profile
Be Honest, Be Harder, Be Stronger

0개의 댓글