[Unity] Space-Shooter (1)

suhan0304·2024년 6월 17일

유니티 - SpaceShooter

목록 보기
1/6
post-thumbnail

3D TPS 게임을 개발해보자. 이 프로젝트 리뷰 글을 자세한 개발 과정 및 방법보다는 개발 내용을 위주로 작성할 예정이다. (추가적으로 알아두면 좋을만한 정보는 작성할 예정)


Asset Import

아래의 무료 에셋을 import해서 사용한다.

https://assetstore.unity.com/packages/2d/textures-materials/metals/yughues-free-metal-materials-12949

https://assetstore.unity.com/packages/2d/textures-materials/sky/skybox-volume-2-nebula-3392

https://assetstore.unity.com/packages/3d/props/industrial/barrel-840


Update Log

Base 제작

Player 추가 및 애니메이션 적용

  • Lod Group을 사용해서 LOD를 적용
  • Follow Camera 로직을 구현

Points

Edit > Preferences 의 Scene View > Create Objects at Origin 을 체크해주면 항상 새로 생성하는 오브젝트의 Position 값을 0, 0, 0으로 설정해준다.

Edit > Project Settings 에서 Editor의 Noumbering Scheme에서 Object Naming을 변경하면서 오브젝트의 복제본 이름 설정을 해줄 수 있다.

하늘 표현

기본적으로 하늘 표현은 SkyBox와 SkyDome, Panoramic, Proecdural 방식이 있는데 skybox는 여섯 방면의 이미지를 큐브 형태로 배치 표현한다. skydome은 돔 형태의 메시에 하늘의 이미지 텍스트를 입혀 구현한다.

위와 같이 여섯 방면에 각각의 이미지를 연결시켜줘서 사용할 수 있다. (Down 이미지는 없는 경우도 있다.) 이렇게 생성한 SkyBox Material을 Window > Rendeering > Lighting의 Environment 탭의 Skybox Material에 연결해하면 다음과 같이 하늘이 바뀐다.

Procedural 방식은 유니티를 처음 열었을때 적용된 하늘로 텍스쳐를 적용하지 않고 색상, 대기 농도, 노출, 태양의 위치와 크기를 설정할 수 있다.

이 때 Lighting의 Sun Source에 Directional Light가 연결시키면 Directional Light의 각도에 따라 태양 이미지의 위치가 변경된다.

이 외에도 큐브맵 스카이박스가 있는데 먼저 큐브맵을 생성한 후에 하늘을 표현한다. 큐브맵은 여섯 방면 스카이박스와 마찬가지로 6장의 텍스처가 필요하며 주변 환경을 반사하는 효과에 주로 사용한다.

여섯 방면 스카이박스는 6개의 텍스처를 사용하므로 기본적으로 6 드로우콜(DrawCall)을 소모하지만, 큐브맵 스카이박스와 Procedural 스카이박스는 1 드로우콜만 소모하므로 좋은 대안이 될 수 있다. (Sky Dome 또한 1장의 텍스처를 사용하면 1 드로우콜만 소모한다. 하지만 카메라가 볼 수 있는 최대거리(Far Clipping Plane)을 제한해야 할 때는 Sky Dome 방식을 적용하는 것은 적합하지 않을 수 있다.

이 외에도 어떤 렌더 파이프라인을 쓰냐에 따라 다양한 하늘 표현이 가능하다. 공식 문서를 한 번 읽어보면 도움이 된다. 사용하는 렌더 파이프라인에 따른 필요한 하늘 표현 방식을 적절히 선택하는게 중요하다.

유니티 엔진 개발 방식

유니티 엔진의 개발 방식은 컴포넌트 기반의 개발 방식와 멀티스레드 기반의 DOTS 개발 방식이 있다. (시간이 DOTS에 대해서 어느정도 공부해보는 것도 좋아보인다.)

컴포넌트 기반의 개발 방식은 지금 내가 개발하는 방식으로 독립적인 기능 단위의 컴포넌트들을 조립하는 방식으로 개발해 나가는 방식이다.

유니티의 주요 이벤트 함수

실행 순서는 아래와 같다.

  • 한 번만 : Awake > OnEnable > Start
  • 프레임마다 : (Fixed)Update > (Fixed)LateUpdate

Awake는 스크립트가 비활성화 돼 있어도 호출된다. OnEnable은 스크립트가 활성화 될 때 호출된다.
Update는 화면의 렌더링 주기와 일치하여 호출 간격이 불규칙적이다. 즉, 정말 동일 프레임으로 고정되면 모두 동일하겠지만 컴퓨터 성능이나 순간 렌더링 리소스 양에 따라서 프레임이 변동적이면 Update도 불규칙적으로 호출된다. 그렇기 때문에 주로 FixedUpdate를 써서 물리 엔진의 계산 주기와 일치시켜 일정한 간격으로 호출되도록 한다. (기본값은 0.02초)이다.

잘못 추가된 에셋 찾기

Find References를 사용하면 프로젝트 내의 어떤 에셋이 그 스크립트를 컴포넌트로 가지고 있는지를 바로 찾을 수 있다. (스크립트 뿐만 아니라 유니티의 모든 에셋도 동일하게 사용 가능하다.)

컴포넌트 캐시 처리

업데이트 함수는 매 프레임마다 호출되기 때문에 최적화에 주의를 기울여야한다. "transform." 처럼 매 프레임마다 Trasfrom 컴포넌트에 접근하는 방식은 바람직하지 않다. 따라서 이를 미리 변수에 담아 두고 해당 변수에 접근하는 방식이 미세하지만 빠르다. 따라서 컴포넌트 캐시 처리란 Awake, Start에서 컴포넌트를 미리 변수에 할당한 후에 접근하는 것을 말한다.

private Transform tr;
void Start()
{
    tr = GetComponent<Transform>();
}

애니메이션

유니티는 레거시 애니메이션과 메카님 에니메이션 두 가지 유형의 애니메이션을 지원한다.

  • 레거시 애니메이션 : 하위 호환성을 고려한 애니메이션, 소스 코드로 컨트롤 해야함
  • 메카님 애니메이션 : 모션 캡처 애니메이션, 리타게팅 기능

애니메이션 클립을 애니메이션 파일로 만드는 방법에는 세 가지가 있다.

  1. 모든 애니메이션 클립이 하나의 애니메이션 파일에 있는 경우와 각 애니메이션 클립이 시작, 종료 프레임을 가지는 방식이다. 전자의 경우에는 클립을 직접 분리해서 사용해야 한다.

  2. 미리 분리된 경우, 그냥 사용하면 된다.

  3. 동작별로 분리해서 별도의 파일로 생성하는 방식

    • 이 때 파일명은 "모델명@애니메이션 클립명" 형태

무기 장착

무기류를 장착하려면 주인공 캐릭터의 본(Bone) 구조를 확인해야 한다. 즉, 오른손 관절을 찾아 내려가야한다. 무기류를 장착할 수 있는 지점(Holder)를 만들어 제공하는 경우라면 좀 더 편하게 작업할 수 있다.

그림자 설정

세 가지 광원(Directional Light, Point Light, Spotlight)에 실시간 그림자를 지원한다. 그림자 타입은 아래와 같다.

  • No Shadows : 기본 설정값 (실시간 그림자 적용 X)
  • Hard Shadows : 실시간 그림자, 외곽선에 계단 현상
  • Soft Shadows : 부드러운 실시간 그림자, 가장 많은 부하를 준다.

그림자는 언제나 항상 엔진에 많은 부하를 준다. 따라서 실시간 그림자 효과가 필요 없는 3D 모델은 그림자 영향에서 제외한다.

  • Cast Shadows : 빛을 받아서 자신의 그림자를 만들 것인지 결정하는 속성

  • Receive Shadows : 다른 그림자에 들어갔을 때 표면에 그림자의 영향을 받는지 아닌지

메쉬를 이용한 그림자

실시간 그림자보다는 시각적인 효과는 덜하지만, 입체갑을 낼 수 있는 가벼운 그림자를 구현할 수 있다. 단순한 평면 메시를 이용하는 방법으로 모바일 게임에서 흔히 볼 수 있는 방식이다.

Quad 오브젝트를 발바닥 부분에 눕혀서 생성해준 후에 BlobShadow Material을 적용해준다.

Shadow(Quad)의 Shader를 Mobile > Particles > Multiply로 변경하면 아래와 같은 자연스러운 그림자 효과가 표현된다.

LOD ( LOD Group )

플레이어의 경우, LOD(Level Of Detail)은 카메라와의 거리에 따라서 다른 폴리곤을 렌더링하기 위해 모델을 합쳐놓았다.

LOD Group 컴포넌트를 추가한 다음에 LOD 구간에 따라 메시를 연결해두면 LOD 기능을 사용할 수 있다.

위의 Culled는 카메라와의 거리가 아주 멀리 떨어져 아예 렌더링하지 않는 구간을 의미한다. 구간의 개수와 범위 또한 변경할 수 있다.


Scripts

PlayerCtrl.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerCtrl : MonoBehaviour
{
    private Transform tr;
    private Animation anim;

    public float moveSpeed = 10f;
    public float turnSpeed = 80f;
    void Start()
    {
        tr = GetComponent<Transform>();
        anim = GetComponent<Animation>();

        anim.Play("Idle");
    }

    void Update()
    {
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");
        float r = Input.GetAxis("Mouse X");

        Vector3 moveDir = (Vector3.forward* v) + (Vector3.right) * h;

        tr.Translate(moveDir.normalized * moveSpeed * Time.deltaTime);

        tr.Rotate(Vector3.up * turnSpeed * Time.deltaTime * r);
    
        PlayerAnim(h, v);
    }

    void PlayerAnim(float h, float v) {
        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);
        }
    }
}

FollowCam.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class FollowCam : MonoBehaviour
{
    public Transform targetTr;
    private Transform camTr;

    [Range(2.0f, 20.0f)]
    public float distance = 10.0f;

    [Range(0.0f, 10.0f)]
    public float height = 2.0f;

    // 반응 속도
    public float damping = 10.0f;
    public float targetOffset = 2.0f;
    private Vector3 velocity = Vector3.zero;

    void Start() {
        camTr = GetComponent<Transform>();
    }

    void LateUpdate() {
        Vector3 pos = targetTr.position
                        + (-targetTr.forward * distance)
                        + (Vector3.up * height);
        
        camTr.position = Vector3.SmoothDamp(camTr.position, pos, ref velocity, damping);

        camTr.LookAt(targetTr.position + (targetTr.up * targetOffset));    
    }
}
profile
Be Honest, Be Harder, Be Stronger

0개의 댓글