프로젝트뷰에 스크립트를 하나만들고 이름을 FollowCam으로 변경한다.

다음 코드를 입력한다.

[Range(2.0f,20.f)]는 다음줄의 퍼블릭 변수의 범위를 지정해준다.

LateUpdate()는 모든 Update()함수가 실행되고 난 후에 호출되는 함수다. 이 함수는 주인공캐릭터가 이동을 완료한후 카메라를 이동시키기위해 사용된다.

Camera의 위치를 Player의 뒤쪽으로 이동시킨다. 이때 카메라의 높이도 약간 올려준다.

LootAt()이라는 함수로 Camera를 Rotate시킨다. 이때 시선을 약간 위로하기 위해 조정이 필요해서 Vector3.up을 더해 줬다

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

public class FollowCam : MonoBehaviour
{
    // Start is called before the first frame update
    public Transform targetTr;
    private Transform camTr;
    [Range(2.0f, 20.0f)]  //따라갈 대상으로 부터 떨어질 거리
    public float distance = 4.0f;
    [Range(0f, 10.0f)] // Y축으로 이동할 높이
    public float height = 2.0f;
    void Start()
    {
        camTr= GetComponent<Transform>();  // 자신의 Transform참조를 camTr에 저장
    }

    // Update is called once per frame
    void LateUpdate()
    {   //추적해야할 대상의 뒤쪽+위쪽 으로 이동
        camTr.position = targetTr.position + -targetTr.forward * distance + Vector3.up* height;
        camTr.LookAt(targetTr.position+Vector3.up);  //Camera를 피봇 좌표를 향해 회전
    }
}

코드를 하이라키의 Main Camera에 적용하고. Main Camera의 Inspector>FollowCam Scriptor컴포넌트에 Palyer를 끌어다 놓는다.

오 카메라가 캐릭터 뒤에서를 잘따라 다닌다. 점도 부드럽게 이동시키기 위해 시간간격을 추가해 보겠다.

 

Vector3.Lerp, Vector3.Slerp

선형보간(Linear Interpolation)과 구면 선형보간(Spherical Linear Interpolatoin)은 시작점과 끝점 사이의 특정 위치의 값을 추정할 때 사용한다.이러한 보간 함수는 현재 값을 목표값으로 변경할 때 갑자기 변경하지 않고 부드럽게 변경시키는 로직에 많이 활용된다.

Lerp는 선형보간 함수를 제공하며 Vector3, Mathf, Quaternion, Color구조체에서 사용할 수 있다.

Vector3.Lerp(시작좌표, 종료좌표, t);
Mathf.Lerp(시작좌표, 종료좌표, t);
Quaternion.Lerp(시작좌표, 종료좌표, t);

Slerp는 구형 선형 보간함수를 제공하며 Vector Quaternion에서 사용할수 있다.

 

FollowCam 스크립트를 Slerp를 사용하는 방식으로 수정한다.

        Vector3 pos = targetTr.position + -targetTr.forward * distance + Vector3.up* height;
        camTr.position = Vector3.Slerp(camTr.position, pos, Time.deltaTime*damping);

이전에는 Player의Position을 직접Camera에 넣어 무조건 따라붙게 만들었는데 이번에는 Vector3.Slerp(camTr.position, pos, Time.deltaTime*damping);가 천천히 따라 붙게 만든다 서있으면 카메라가 천천히 따라오다 서는 걸 느낄수 있다.

실제 실행시켜보면 Slerp는 Lerp에 비해 시작과 끝에서 좀더 반응속도가 빠르다.

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

public class FollowCam : MonoBehaviour
{
    // Start is called before the first frame update
    public Transform targetTr;
    private Transform camTr;
    [Range(2.0f, 20.0f)]  //따라갈 대상으로 부터 떨어질 거리
    public float distance = 4.0f;
    [Range(0f, 10.0f)] // Y축으로 이동할 높이
    public float height = 2.0f;
    public float damping = 10.0f;
        void Start()
    {
        camTr= GetComponent<Transform>();  // 자신의 Transform참조를 camTr에 저장
    }

    // Update is called once per frame
    void LateUpdate()
    {   //추적해야할 대상의 뒤쪽+위쪽 으로 이동
        Vector3 pos = targetTr.position + -targetTr.forward * distance + Vector3.up* height;
        camTr.position = Vector3.Slerp(camTr.position, pos, Time.deltaTime*damping);
        camTr.LookAt(targetTr.position+Vector3.up);  //Camera를 피봇 좌표를 향해 회전
    }
}

 

Vector3.SmoothDamp

부드럽게 이동하는 방법중 Vector3.SmoothDamp도 있다. 많이 사용된다. 가까워질수록 느려진다.

SmoothDamp(시작벡터, 목표벡터, 현재속도, 도달시간)

소스를 다음과 같이 고쳐보자 플레이해보면 엄청 늦게 따라간다. Slerp의 시간은 진행시간이고 smoothDamp의 시간은 이동에 걸리는 시간이라 작아야 빨라진다. 인스펙트뷰 에서 bumping 변수를 0.1로 하면 빨라진다. 1정도로 해도 멋진 장면이 연출된다.

velocity 는 ref 키워드로 참조를 전달하는데, Debug.Log 로 값을 찍어보면 지속적으로 갱신되는 속도값을 얻고, 다음 프레임에 다시 전달하고 있다는 걸 알 수 있습니다.

 

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

public class FollowCam : MonoBehaviour
{
    // Start is called before the first frame update
    public Transform targetTr;
    private Transform camTr;
    [Range(2.0f, 20.0f)]  //따라갈 대상으로 부터 떨어질 거리
    public float distance = 4.0f;
    [Range(0f, 10.0f)] // Y축으로 이동할 높이
    public float height = 2.0f;
    public float damping = 10.0f;
    private Vector3 velocity = Vector3.zero;
        void Start()
    {
        camTr= GetComponent<Transform>();  // 자신의 Transform참조를 camTr에 저장
    }

    // Update is called once per frame
    void LateUpdate()
    {   //추적해야할 대상의 뒤쪽+위쪽 으로 이동
        Vector3 pos = targetTr.position + -targetTr.forward * distance + Vector3.up* height;
        //camTr.position = Vector3.Slerp(camTr.position, pos, Time.deltaTime*damping);
        camTr.position = Vector3.SmoothDamp(camTr.position, pos, ref velocity, damping);
        camTr.LookAt(targetTr.position+Vector3.up);  //Camera를 피봇 좌표를 향해 회전
    }
}

보간

Unity의 보간 함수를 사용하면 주어진 두 점 사이의 값을 계산할 수 있습니다. 이러한 각 함수는 상황에 맞게 다른 방식으로 작동합니다. 자세한 내용은 각각의 예제를 참조하십시오.

Player 는 LOD(Level of Detail)이라는 기능을 위해 3개의 폴리곤으로 이뤄진 모델이다. LOD는 Character와 카메라간의 거리에 따라 렌더링할 메시를 결정해준다.

 

Player를 선택하고 Add Component>LOD Group컴포넌트를 추가한후 slade_mesh0 1 2를 끌어다   LOD 0 1 2 에 끌어다 놓는다

멀티플레이게임 또는 모바일게임 최적화의 중요한 기능이다.

하이라키뷰에서 Directional Light를 선택하고 인스펙터뷰에서 Shadow Type을 Hard로 바꾼다. Soft는 사실적이지만 부하가 무겁다. RealTime Shadows Strength를 적당히 낮추고 Resolution을 Low로 해서 부담을 더 줄일수 있다.

3D 모델은 Mesh Render 또는 Skinned Mesh Renderer중 한 컴포넌트를 반드시 갖고 있다. 여기는 그림자와 관련된 Cast Shadows와 Receive Shadows 속성이 있다.  실시간 그림자의 영향을 받지 않아도 시각적 효과에 크게 상관없는 3D모델은 이 두가지 옵션을 적절히 설정해 그림자를 생성하지 않게 한다.

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

Two Sided : Plane 또는 Quad처럼 단면만 렌더링하는 모델은 Cast Shadows 속성을 On하고 빛을 모델의 뒷쪽으로 비추면 그림자가 생성되지 않는다. 이때 Cast Shadow 속성을 Two Sided로 하면 그림자가 생성된다.

 

메시를 이용한 그림자

실시간 그림자보다는 효과가 약하지만 갸벼운 그림자 처리를 구현해보자. 단순한 평면 메시를 이용하는 방법으로 모바일 게임에서 흔히 볼 수 있는 방식이다.

먼저 Player에 적용된  실시간 그림자가 생성되지 않도록 설정한다.하이라키뷰의 Player를 펼쳐 slade_mesh, slade_meshod1, slade_meshod2의 세개의 메시로 구성돼 있다. 모두 선택한후 인스텍터뷰에서 Skinned Mesh Renderer 컴포넌트의 Cast Shadows 속성을 Off로 변경하고  Receive Shadows속성도 언체크한다.

들고있는 총도 같은 처리를 해줘 그림자를 Off한다.

하이라키뷰에서Player를 선택후 우클릭후 3D Object>Quad모델을 생성하면 자동으로 Player의 차일드가 된다.

Quad를 선택하고 인스펙터뷰에서 Transform을 다음과 같이 변경한다. Y position을 0.01로 한 이유는 Floor와 겹치는 부분이 깜빡이는 현상이 발생하기 때문이다.

유니티의 원시모델은 기본으로 Collider컴포넌트가 있는데 Quad에서는 필요없으므로 이를 제거한다.

Quad의 이름을 Shadow로 변경한다. Resorces/Texture폴더에서 BlobShadow.psd파일을 Images폴더로 복사한뒤 Shadow(Quad)에 끌어다 적용한다. Material 폴더안에 자동으로 동일한 이름의 머터리얼이 생성된다

Shadow(Quad)선택후 인스펙터에서 필요없는 Cast Shadows를 Off하고 Bos Shadow Shader>Mobile>Particles>Multiply를 선택하면 잘 적용된다. 부하는 적어지지만 바닥이 경사로나 굴곡이 있다면 제대로 표현되지 않는다.

무기장착을 위해 Resources/Models 폴더에서 Wepons 패키지를 임포트 한다.

패키지를 임포트한 후 Weapon폴더를 Model 폴더 하위로 옮긴다.

 

무기를 장착하려면 주인공 캐릭터의 Bone구조를 확인해야한다.

하이라키뷰의 Player를 펼쳐보면 pelvis가 있다. 이는 관절의 정보를 담고 있는 오브젝트로 골반을 의미한다. 여기서 Rweapnholder나 없다면 Rwrist관절에 무기류를 장착한다.

 

임포트한 Weapons의 w_rifle01모델을 Rweaponholder로 끌어다 놓는다 다른총모델도 무관하다.

오른손에 총이 잘 붙어 있지만 받치고 있는 왼손이 좀 떨어져 있다.

하이라키의 라이플을 선택하고 transform을 리셋한다.

이제 받치는 손이 잘 붙어 있다

'유니티게임강좌 > 주인공 캐릭터 제작' 카테고리의 다른 글

[Player제작] Level of Detail 설정  (0) 2023.02.26
[Player제작] 그림자  (0) 2023.02.26
[Player제작] 애니메이션  (1) 2023.02.26
[Player제작] 캐릭터 회전 - Rotate  (0) 2023.02.25
Assembly  (0) 2023.02.25

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

  • 레거시 애니메이션 : 하위 호환성을 고려한 애니메이션, 소스코드로 컨트롤 함. 가볍지만 새로 만든다면 메카님을 추천
  • 메카님 애니메이션 : 모션 캡쳐 애니메이션, 애니메이션을 재사용하는 Retargeting 가능. 

주인공 캐릭터는 애니메이션 타입으로 구현하고 적 캐릭터는 메카님 애니메이션을 적용해보자 

Resorces/Anmations 폴더에서 PlayerAnimation 패키지를 임포트한다.

임포트후 생성된 폴더는 07.Animations폴더로 옮긴다.

프로젝트뷰 Model/Player폴더내의 Player모델을 선택 인스펙터뷰의 위쪽  Rig탭을 선택하고 Animation Type을 Leagacy로 설정한다.

프로퍼티:기능:

Animation Type 애니메이션 타입을 지정합니다.
  None 애니메이션이 없습니다.
  Legacy 레거시 애니메이션 시스템을 사용합니다. Unity 3.x 및 이전 버전과 동일하게 애니메이션을 임포트하고 사용합니다.
  Generic 릭이 비휴머노이드(네 발 달린 생물 또는 애니메이션화할 엔티티)인 경우 제니릭 애니메이션 시스템을 사용합니다. Unity가 루트 노드를 선택하지만, 루트 노드(Root node) 로 사용할 다른 뼈대를 대신 식별할 수 있습니다.
  Humanoid 릭이 휴머노이드(두 다리와 두 팔과 머리가 있음)인 경우 휴머노이드 애니메이션 시스템을 사용합니다. 일반적으로 Unity가 골격을 감지하고 아바타에 올바르게 매핑합니다. 경우에 따라 아바타 정의(Avatar Definition) 변경사항을 설정하고 매핑을 수동으로 설정(Configure) 해야 할 수 있습니다.

하이라키뷰에서 Player의 Inspector를 보면 Animation컴포넌트가 추가되어 있다 이는 레거시타입으로 설정된 모델을 의미한다.  Animator가 추가되어 있다면 메카님 애니메이션이다.

애니메이션 클립

애니메이션 클립은 캐릭터의 동작을 기록한 파일입니다. 애니메이션 컴포넌트는 클립에 기록된 관절의 위치와 회전값을 프레임 단위로 재생시키는 역할을 한다.

3D 모델링 툴에서 제작한 애니메이션 클립을 애니메이션 파일로 만드는 방법에는 3가지가 있다.

  • 모든 애니메이션이 하나의 애니메이션 파일에 들어 있고가 있는데 분리 해야하는 경우
  • 모든 애니메이션 클립이 하나의 애니메이션 파일에 들어가 있지만 미리 분리된 경우
  • 애니메이션 클립들이 동작별로 분리해 별도의 파일로 생성하는 방식

애니메이션 적용

애니메이션폴더의 idel을 클릭해서 Inspector뷰를 보면 다음과 같이 아무것도 없다.

씬뷰나 프로젝트뷰의 모델 Player를 끌어다 idel Inspector뷰 맨아리 프리뷰에 끌어다 놓으면 모델이 보인다.

Zoom과 클릭후 Move해서 보기좋게 한후 Play를 누르면 프리뷰를 볼 수 있다.

Player Animation Clip폴더에서 Idel, RunB, RunF, RunL, RunR의 클립만 사용할텐데 Wrap Mode를 Loop로 지정한다. 한꺼번에 할수 없어 개별로 하나씩 해야한다.

하이라키의 Player를 선택한후 자물쇠를 잠구고 Animation Clip폴더에서 Idel, RunB, RunF, RunL, RunR 개를 전부 골라 끌어다 인스펙터의 Animations에 끌어다 놓는다 그럼 오른쪽 숫자가 0에서 5로 바뀐다. 열어보면 5개의 클립이 잘 적용되여 있다. 

이제 처음 실행시킬 Idle 애니메이션을 인스펙터의 Animation에 끌어다 놓으면 None이 Idle로 바뀐다.

설정완료후는 우상쪽 Lock을 푼다. Play해보면 Player가 Idel클립을 반복한다. 한번만 하고 끝난다면 아까 Wrap Mode를Loop로 설정 안한거다.

Player Inspector뷰의 Play Automatically속성은 실행했을때 기본 애니메이션 클립을 자동으로 실행해주는 옵션인데 해제한다. 이러면 play해도 아무런 동작도 하지 않는다.

스크립트를 통해 직접 컨트롤 해보겠다. PlayerCtrl스크립트를 다음과 같이 수정한다.

 

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

public class PlayerCtrl : MonoBehaviour {
    // Start is called before the first frame update
    Transform tr;
    private float moveSpeed;
    private float turnSpeed;
    private Animation anim;
    void Start() {

        tr = GetComponent<Transform>();
        moveSpeed = 10f;
        turnSpeed = 500f;
        anim = GetComponent<Animation>();  //추가된 코드
        anim.Play();
    }

    // Update is called once per frame
    void Update() {

        float h = Input.GetAxis("Horizontal");  //AD 입력 좌우
        float v = Input.GetAxis("Vertical");  //WS 입력 전후
        float r = Input.GetAxis("Mouse X");  //마우스 x축 입력
        Vector3 dir = Vector3.right* h + Vector3.forward* v;
        tr.Translate(dir.normalized * moveSpeed * Time.deltaTime);
        tr.Rotate(Vector3.up * turnSpeed * Time.deltaTime * r);
    }
}

Play해 보면게임뷰에서 캐릭터가 Idle동작을 반복해준다.

이제 키보드 동작에 맞춰 애니매이션을 바꿔가면 플레이해보겠다.

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

public class PlayerCtrl : MonoBehaviour {
    // Start is called before the first frame update
    Transform tr;
    private float moveSpeed;
    private float turnSpeed;
    private Animation anim;
    void Start() {
        tr = GetComponent<Transform>();
        moveSpeed = 10f;
        turnSpeed = 500f;
        anim = GetComponent<Animation>();  //추가된 코드
        anim.Play();
    }

    // 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.Play("RunF");
        } else if(v <= -0.1f) {
            anim.Play("RunB");
        } else if(h >= 0.1f) {
            anim.Play("RunR");
        } else if (h <= -0.1f) {
            anim.Play("RunL");
        } else {
            anim.Play("Idle");
        }
    }
}

너무 잘된다. 현재도 문제없지만 유니티는 애니메이션사이의 전환을 부드럽게 만들어주는 CroffFade라는 함수를 준비해놓왔다. 사용법은 

CrossFace("클립명", 전환시간);

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

public class PlayerCtrl : MonoBehaviour {
    // Start is called before the first frame update
    Transform tr;
    private float moveSpeed;
    private float turnSpeed;
    private Animation anim;
    void Start() {
        tr = GetComponent<Transform>();
        moveSpeed = 10f;
        turnSpeed = 500f;
        anim = GetComponent<Animation>();  //추가된 코드
        anim.Play();
    }

    // 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);
        }
    }
}

실행해보면 큰차이를 모르겠지만 동작간 차이가 클 경우 필요할 듯하다.

 

#유니티 #Animation

'유니티게임강좌 > 주인공 캐릭터 제작' 카테고리의 다른 글

[Player제작] 그림자  (0) 2023.02.26
[Player제작] 무기장착  (0) 2023.02.26
[Player제작] 캐릭터 회전 - Rotate  (0) 2023.02.25
Assembly  (0) 2023.02.25
[Player제작] 접근제한자  (0) 2023.02.25

게임오브젝트를 회전시킬 때는 Transform.rotation 속성값을 변경하거나 Rotate계열의 함수를 사용할 수 있다. 

다음은 오브젝트를 y축으로 30도 회전시키는 예이다.

tr.Rotate(new Vector3(0.0f, 30.f, 0.0f);
tr.Rotate(0.0f, 30.0f, 0.0f);
tr.Rotate(Vector3.up * 30f);

마우스입력을 받아 캐릭터가 회전하게 해보겠다. 마우스 움직임이 작기 때문에 스피드가 빨라야 한다.

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

public class PlayerCtrl : MonoBehaviour {
    // Start is called before the first frame update
    Transform tr;
    private float moveSpeed;
    private float turnSpeed;
    void Start() {
        tr = GetComponent<Transform>();
        moveSpeed = 10f;
        turnSpeed = 500f;
    }

    // Update is called once per frame
    void Update() {
        float h = Input.GetAxis("Horizontal");  //AD 입력 좌우
        float v = Input.GetAxis("Vertical");  //WS 입력 전후
        float r = Input.GetAxis("Mouse X");  //마우스 x축 입력
        Vector3 dir = Vector3.right* h + Vector3.forward* v;
        tr.Translate(dir.normalized * moveSpeed * Time.deltaTime);
        tr.Rotate(Vector3.up * turnSpeed * Time.deltaTime * r);
    }
}

씬뷰에서 가상 카메라 이동

씬뷰와 게임뷰를 동시에 표시하고

하이라키에서 Player를 클릭한후 Shift-F를 누르면  Play해보면 씬뷰의 카메라는 Player에 Lock된다.

'유니티게임강좌 > 주인공 캐릭터 제작' 카테고리의 다른 글

[Player제작] 무기장착  (0) 2023.02.26
[Player제작] 애니메이션  (1) 2023.02.26
Assembly  (0) 2023.02.25
[Player제작] 접근제한자  (0) 2023.02.25
[Player제작] 캐릭터의 이동  (0) 2023.02.25

접급제한자 internal을 공부하다가 어셈블리가 나와 유니티 매뉴얼을 보니 같은 라이브러리 같습니다. 유니티는 어셈블리를 관리하는 메뉴가 있네요. 초보분들은 그냥 넘어가는게 나을듯 합니다.

어셈블리 정의

어셈블리 정의 및 어셈블리 레퍼런스는 스크립트를 어셈블리로 구성할 수 있는 에셋입니다.

어셈블리는 스크립트에 의해 정의되고 다른 어셈블리에 대한 레퍼런스도 정의하는 컴파일된 클래스와 구조체가 들어 있는 C# 코드 라이브러리입니다. C#의 어셈블리에 대한 일반적인 정보는 .NET의 어셈블리를 참조하십시오.

기본적으로 Unity는 거의 모든 게임 스크립트를 사전 정의된 어셈블리인 Assembly-CSharp.dll로 컴파일합니다. (또한 Unity는 [몇 가지 더 작고 전문화된 사전 정의 어셈블리]도 생성합니다.)

이 배열은 소규모 프로젝트에 적합하지만, 프로젝트에 코드를 더 추가할 경우 몇 가지 단점이 있습니다.

  • 하나의 스크립트를 변경할 때마다 Unity는 다른 모든 스크립트를 다시 컴파일해야 하므로 반복적 코드 변경을 위한 전체 컴파일 시간이 늘어납니다.
  • 모든 스크립트는 다른 스크립트에서 정의된 타입에 직접 액세스할 수 있으므로 코드를 리팩터링하고 개선하기가 더 어려울 수 있습니다.
  • 모든 스크립트는 모든 플랫폼에 대해 컴파일됩니다.

어셈블리를 정의하면 코드를 구성하여 모듈성과 재사용성을 높일 수 있습니다. 프로젝트에 대해 정의하는 어셈블리의 스크립트는 더 이상 기본 어셈블리에 추가되지 않으며, 지정하는 다른 어셈블리의 스크립트에만 액세스할 수 있습니다.

위 다이어그램은 프로젝트의 코드를 여러 어셈블리로 분할하는 방법을 보여줍니다. Main은 Stuff를 참조하고 그 반대로는 참조하지 않기 때문에 Main의 코드 변경 사항은 Stuff의 코드에 영향을 주지 않습니다. 마찬가지로 Library는 다른 어셈블리에 의존하지 않으므로 다른 프로젝트의 Library에 있는 코드를 더 쉽게 재사용할 수 있습니다.

이 섹션에서는 어셈블리 정의 및 어셈블리 레퍼런스 에셋을 만들고 설정하여 프로젝트에 대해 어셈블리를 정의하는 방법에 대해 설명합니다.

어셈블리 정의

프로젝트 코드를 어셈블리로 구성하려면 원하는 각 어셈블리에 대한 폴더를 생성하고, 각 어셈블리에 속해야 하는 스크립트를 관련 폴더로 옮겨야 합니다. 그런 다음 [어셈블리 정의 에셋을 생성]하여 어셈블리 프로퍼티를 지정하십시오.

Unity는 어셈블리 정의 에셋이 포함된 폴더의 모든 스크립트를 가져온 후 에셋에 정의된 이름 및 기타 설정을 사용하여 어셈블리로 컴파일합니다. 또한 Unity는 자식 폴더에 자체 어셈블리 정의 또는 어셈블리 레퍼런스 에셋이 없는 경우 동일한 어셈블리의 모든 자식 폴더에도 스크립트를 포함합니다.

기존 어셈블리에 자식이 아닌 폴더의 스크립트를 포함하려면 자식이 아닌 폴더에 어셈블리 레퍼런스 에셋을 생성하고, 타겟 어셈블리를 정의하는 어셈블리 정의 에셋을 참조하도록 설정하십시오. 예를 들어 해당 폴더의 위치에 관계없이 프로젝트에 있는 모든 에디터 폴더의 스크립트를 자체 어셈블리에 결합할 수 있습니다.

Unity는 종속성에 의해 결정된 순서대로 어셈블리를 컴파일합니다. 컴파일이 수행되는 순서는 지정할 수 없습니다.

 

레퍼런스 및 종속성

한 타입(예: 클래스 또는 구조체)이 다른 타입을 사용하는 경우 첫 번째 타입은 두 번째 타입에 종속됩니다. Unity가 스크립트를 컴파일할 때 스크립트가 종속되는 모든 타입 또는 기타 코드에 액세스할 수 있어야 합니다. 마찬가지로, 컴파일된 코드가 실행될 때 컴파일된 버전의 종속성에 액세스할 수 있어야 합니다. 두 타입이 서로 다른 어셈블리에 있는 경우 종속 타입을 포함하는 어셈블리는 종속되는 타입을 포함하는 어셈블리에 대한 레퍼런스를 선언해야 합니다.

어셈블리 정의 옵션을 사용하여 프로젝트에 사용된 어셈블리 간의 레퍼런스를 제어할 수 있습니다. 어셈블리 정의 설정에는 다음이 포함됩니다.

  • [Auto Referenced] – 사전 정의된 어셈블리가 어셈블리를 참조하는지 여부
  • [Assembly Definition References] – 어셈블리 정의를 사용하여 생성된 다른 프로젝트 어셈블리에 대한 레퍼런스
  • [Override References] + [Assembly References] – 사전 컴파일된 (플러그인) 어셈블리에 대한 레퍼런스
  • [No Engine References] – UnityEngine 어셈블리에 대한 레퍼런스

참고: 어셈블리 정의를 사용하여 생성된 어셈블리의 클래스는 사전 정의된 어셈블리에서 정의된 타입을 사용할 수 없습니다.

 

기본 레퍼런스

기본적으로 사전 정의된 어셈블리는 어셈블리 정의를 사용하여 생성된 어셈블리(1), 프로젝트에 플러그인으로 추가된 사전 컴파일된 어셈블리(2)를 비롯한 다른 모든 어셈블리를 참조합니다. 또한 어셈블리 정의 에셋을 사용하여 생성된 어셈블리는 모든 사전 컴파일된 어셈블리(3)를 자동으로 참조합니다.

기본 설정의 경우 사전 정의된 어셈블리의 클래스는 프로젝트의 다른 어셈블리에서 정의된 모든 타입을 사용할 수 있습니다. 마찬가지로, 어셈블리 정의 에셋을 사용하여 생성된 어셈블리는 사전 컴파일된 (플러그인) 어셈블리에서 정의된 모든 타입을 사용할 수 있습니다.

어셈블리 정의 에셋에 대한 인스펙터에서 [Auto Referenced 옵션]을 비활성화하여 사전 정의된 어셈블리에서 어셈블리를 참조하지 못하도록 막을 수 있습니다. Auto Referenced 옵션을 비활성화하면 어셈블리에서 코드를 변경할 때 사전 정의된 어셈블리가 다시 컴파일되지 않지만, 사전 정의된 어셈블리가 이 어셈블리의 코드를 직접 사용할 수 없습니다. [어셈블리 정의 프로퍼티]를 참조하십시오.

마찬가지로, 플러그인 에셋에 대한 플러그인 인스펙터에서 Auto Referenced 프로퍼티를 비활성화하여 플러그인 어셈블리가 자동으로 참조되지 못하게 막을 수 있습니다. 이는 사전 정의된 어셈블리, 그리고 어셈블리 정의를 사용하여 생성된 어셈블리 모두에 영향을 미칩니다. 자세한 내용은 플러그인 인스펙터를 참조하십시오.

플러그인 에 대한 Auto Referenced 를 비활성화하면 인스펙터에서 어셈블리 정의 에셋에 대해 명시적으로 레퍼런스를 설정할 수 있습니다. 에셋의 [Override References] 옵션을 활성화하고 플러그인 에 대한 레퍼런스를 추가하십시오. Assembly Definition 프로퍼티를 참조하십시오.

참고: 사전 컴파일된 어셈블리에 대한 명시적 레퍼런스는 선언할 수 없습니다. 사전 정의된 어셈블리는 자동 참조된 어셈블리의 코드만 사용할 수 있습니다.

 

순환 레퍼런스

순환 어셈블리 레퍼런스는 한 어셈블리 레퍼런스가 두 번째 어셈블리를 참조하고, 두 번째 어셈블리는 다시 첫 번째 어셈블리를 참조할 때 존재합니다. 어셈블리 간의 이러한 순환 레퍼런스는 허용되지 않으며, “순환 레퍼런스가 포함된 어셈블리가 감지되었습니다”라는 오류 메시지와 함께 보고됩니다.

일반적으로 어셈블리 간의 이러한 순환 레퍼런스는 어셈블리에서 정의된 클래스 내의 순환 레퍼런스로 인해 발생합니다. 동일한 어셈블리에 있는 클래스 간의 순환 레퍼런스에 대해 기술적으로 유효하지 않은 점은 없지만, 다른 어셈블리의 클래스 간 순환 레퍼런스는 허용되지 않습니다. 순환 레퍼런스 오류가 발생하면 코드를 리팩터링하여 순환 레퍼런스를 제거하거나 상호 참조하는 리팩터링 클래스를 동일한 어셈블리에 배치해야 합니다.

 

어셈블리 정의 에셋 생성

어셈블리 정의 에셋을 만들려면 다음 단계를 따르십시오.

  1. Project 창에서 어셈블리에 포함할 스크립트가 있는 폴더를 찾습니다.
  2. 폴더(메뉴: Assets > Create > Assembly Definition)에 어셈블리 정의 에셋을 생성합니다.
  3. 에셋에 이름을 지정합니다. 기본적으로 어셈블리 파일은 에셋에 할당되는 이름을 사용하지만, Inspector 창에서 이름을 변경할 수 있습니다.

Unity는 프로젝트의 스크립트를 다시 컴파일하여 새로운 어셈블리를 생성합니다. 작업이 완료되면 새로운 어셈블리 정의에 대한 설정을 변경할 수 있습니다.

자식 폴더의 스크립트를 포함하여 어셈블리 정의가 포함된 폴더의 스크립트는 (해당 폴더에 자체 어셈블리 정의 또는 레퍼런스 에셋이 없는 경우) 새로운 어셈블리로 컴파일되고 이전 어셈블리에서 제거됩니다.

 

어셈블리 정의 레퍼런스 에셋 생성

어셈블리 정의 레퍼런스 에셋을 생성하려면 다음 단계를 따르십시오.

  1. Project 창에서 참조된 어셈블리에 포함할 스크립트가 있는 폴더를 찾습니다.
  2. 폴더(메뉴: Assets > Create > Assembly Definition Reference)에 어셈블리 레퍼런스 에셋을 생성합니다.
  3. 에셋에 이름을 할당합니다.
  4. Unity는 프로젝트의 스크립트를 다시 컴파일하여 새로운 어셈블리를 생성합니다. 작업이 완료되면 새로운 어셈블리 정의 레퍼런스에 대한 설정을 변경할 수 있습니다.
  5. 새로운 어셈블리 정의 레퍼런스 에셋을 선택하여 인스펙터 에서 해당 프로퍼티를 확인합니다.
  6. 타겟 어셈블리 정의 에셋을 참조하도록 Assembly Definition 프로퍼티를 설정합니다.
  7. Apply 를 클릭합니다.

자식 폴더의 스크립트를 포함하여 어셈블리 정의 레퍼런스 에셋이 포함된 폴더의 스크립트는 (해당 폴더에 자체 어셈블리 정의 또는 레퍼런스 에셋이 없는 경우) 참조된 어셈블리로 컴파일되고 이전 어셈블리에서 제거됩니다.

 

플랫폼별 어셈블리 생성

특정 플랫폼에 대한 어셈블리를 생성하려면 다음 단계를 따르십시오.

  1. 어셈블리 정의 에셋을 생성합니다.
  2. 새로운 어셈블리 정의 레퍼런스 에셋을 선택하여 인스펙터 에서 해당 프로퍼티를 확인합니다.
  3. Any Platform 옵션을 선택하고 제외할 특정 플랫폼을 선택합니다. 또는 Any Platform을 선택 해제하고 포함할 특정 플랫폼을 선택할 수도 있습니다.
  4. Apply 를 클릭합니다.

플랫폼용 프로젝트를 빌드할 때 어셈블리는 선택된 플랫폼에 따라 포함되거나 제외됩니다.

 

에디터 코드용 어셈블리 생성

에디터 어셈블리를 사용하면 Editor라는 최상위 폴더뿐만 아니라 프로젝트의 모든 위치에 에디터 스크립트를 배치할 수 있습니다.

프로젝트에서 에디터 코드가 포함된 어셈블리를 생성하려면 다음 단계를 따르십시오.

  1. 에디터 스크립트가 포함된 폴더에 [플랫폼별 어셈블리를 생성]합니다.
  2. 에디터 플랫폼만 포함합니다.
  3. 에디터 스크립트가 포함된 추가 폴더가 있는 경우 해당 폴더에 [어셈블리 정의 레퍼런스 에셋을 생성]한 후 이 어셈블리 정의를 참조하도록 설정합니다.

 

테스트 어셈블리 생성

테스트 어셈블리를 사용하면 테스트를 작성하고 Unity TestRunner로 실행할 수 있으며, 테스트 코드를 애플리케이션과 함께 제공하는 코드와 별도로 유지할 수 있습니다. Unity는 테스트 프레임워크 패키지의 일부로 TestRunner를 제공합니다. 테스트 프레임워크 패키지를 설치하고 테스트 어셈블리를 생성하기 위한 지침은 테스트 프레임워크 문서를 참조하십시오.

 

다른 어셈블리 참조

다른 어셈블리의 일부인 C# 타입과 함수를 사용하려면 어셈블리 정의 에셋에서 해당 어셈블리에 대한 레퍼런스를 생성해야 합니다.

어셈블리 레퍼런스를 생성하려면 다음 단계를 따르십시오.

  1. 인스펙터 에서 프로퍼티를 확인하기 위해 레퍼런스가 필요한 어셈블리의 어셈블리 정의를 선택합니다.
  2. Assembly Definition References 섹션에서 + 버튼을 클릭하여 새 레퍼런스를 추가합니다.
  3. 레퍼런스 리스트의 새로 생성된 슬롯에 어셈블리 정의 에셋을 할당합니다.

Use GUIDs 옵션을 활성화하면 새로운 이름을 반영하기 위해 다른 어셈블리 정의의 레퍼런스를 업데이트하지 않고도 참조된 어셈블리 정의 에셋의 파일 이름을 변경할 수 있습니다. (에셋 파일의 메타데이터 파일이 삭제되었거나, Unity 에디터 외부로 파일을 이동할 때 해당 메타데이터 파일을 함께 옮기지 않은 경우 GUID를 초기화해야 합니다.)

 

사전 컴파일된 플러그인 어셈블리 참조

기본적으로 어셈블리 정의를 사용하여 생성된 프로젝트의 모든 어셈블리는 사전 컴파일된 모든 어셈블리를 자동으로 참조합니다. 이러한 자동 참조는 어셈블리의 코드가 사용되지 않더라도 사전 컴파일된 어셈블리 중 하나를 업데이트할 때 Unity가 모든 어셈블리를 다시 컴파일해야 함을 의미합니다. 이러한 추가 오버헤드를 방지하려면 자동 참조를 오버라이드하고 어셈블리가 실제로 사용하는 사전 컴파일된 라이브러리에 대한 참조만 지정하십시오.

  1. 인스펙터 에서 프로퍼티를 확인하기 위해 레퍼런스가 필요한 어셈블리의 어셈블리 정의를 선택합니다.
  2. General 섹션에서 Override References 옵션을 활성화합니다.Override References 를 선택하면 인스펙터  Assembly References 섹션을 이용할 수 있습니다.
  3. Assembly References 섹션에서 + 버튼을 클릭하여 새 레퍼런스를 추가합니다.
  4. 빈 슬롯의 드롭다운 리스트를 사용하여 사전 컴파일된 어셈블리에 레퍼런스를 할당합니다. 리스트에는 프로젝트 빌드 설정에 현재 설정된 플랫폼에 대한 프로젝트의 사전 컴파일된 어셈블리가 모두 표시됩니다. (플러그인 인스펙터에서 사전 컴파일된 어셈블리에 대한 플랫폼 호환성을 설정하십시오.)
  5. Apply 를 클릭합니다.
  6. 프로젝트를 빌드할 각 플랫폼에 대해 이 단계를 반복합니다.

 

조건부 어셈블리 포함

프리 프로세서 심볼을 사용하면 어셈블리가 컴파일되고 게임 또는 애플리케이션의 빌드에 포함되는지 여부를 제어할 수 있습니다(에디터의 플레이 모드 포함). 어셈블리 정의 옵션의 Define Constraints 리스트에서 사용하려는 어셈블리에 대해 정의해야 할 심볼을 지정할 수 있습니다.

  1. 인스펙터 에서 프로퍼티를 확인할 어셈블리의 어셈블리 정의를 선택합니다.
  2. Define Constraints 섹션에서 + 버튼을 클릭하여 제약 리스트에 새로운 심볼을 추가합니다.
  3. 심볼 이름을 입력합니다.
  4. 이름 앞에 느낌표를 붙여서 심볼을 “무효화”할 수 있습니다. 예를 들어 UNITY_WEBGL이 정의되지 않은 경우 !UNITY_WEBGL 제약은 어셈블리를 포함할 수 있습니다.
  5. Apply 를 클릭합니다.

다음 심볼을 제약으로 사용할 수 있습니다.

  • [Scripting Define Symbols] 설정에서 정의된 심볼. 이 심볼은 Project Settings  Player 섹션에서 찾을 수 있습니다. 단, Scripting Define Symbols 는 프로젝트 빌드 설정에서 현재 설정된 플랫폼에만 적용됩니다. 여러 플랫폼에 대한 심볼을 정의하려면 각 플랫폼으로 전환한 후 Scripting Define Symbols 필드를 개별적으로 수정하십시오.
  • Unity에서 정의한 심볼. [플랫폼별 컴파일]을 참조하십시오.
  • 어셈블리 정의 에셋의 [Version Defines] 섹션을 사용하여 정의된 심볼.

스크립트에서 정의된 심볼은 제약이 충족되었는지 판단할 때 고려되지 않습니다.

자세한 내용은 [제약 정의]를 참조하십시오.

 

Unity 및 프로젝트 패키지 버전에 기반하여 기호 정의

프로젝트가 특정한 Unity 버전이나 패키지 버전을 사용하는지에 따라 어셈블리에 다른 코드를 컴파일해야 하는 경우, Version Defines 리스트에 항목을 추가할 수 있습니다. 이 리스트는 기호가 정의되어야 하는 때에 대한 규칙을 지정합니다. 버전 번호에 대해 특정 버전이나 여러 버전으로 평가되는 논리적 표현식을 지정할 수 있습니다.

심볼을 조건부로 정의하려면 다음 단계를 따르십시오.

  1. Inspector 에서 프로퍼티를 볼 어셈블리의 어셈블리 정의 에셋을 선택합니다.
  2. Version Defines 섹션에서 + 버튼을 클릭하여 리스트에 항목을 추가합니다.
  3. 다음 프로퍼티를 설정합니다.
    • Resource: 이 기호를 정의하기 위해 설치해야 하는 패키지나 모듈 또는 Unity 를 선택합니다.
    • Define: 심볼 이름입니다.
    • Expression: 특정 버전 또는 버전 범위로 평가되는 표현식입니다. 규칙은 버전 정의 표현식을 참조하십시오.
    Expression outcome 은 표현식이 어느 버전으로 평가되는지 나타냅니다. 결과에 Invalid 가 표시되면 표현식 구문이 틀린 것입니다.
  4. 다음 예에서는 프로젝트가 Timeline 1.3을 사용하는 경우 USE_TIMELINE_1_3 기호를 정의하며, 프로젝트를 Unity 2021.2.0a7 이상에서 열면 USE_NEW_APIS를 정의합니다.
  5. Apply 를 클릭합니다.

어셈블리 정의에서 정의된 심볼은 해당 정의에 대해 생성된 어셈블리의 스크립트 범위 내에만 있습니다.

Version Defines 리스트를 Define Constraints 로 사용하여 정의된 기호를 사용할 수 있습니다. 따라서 특정 패키지의 특정 버전이 프로젝트에 설치된 경우에만 어셈블리를 사용해야 함을 지정할 수 있습니다.

 

버전 정의 표현식

표현식을 사용하여 정확한 버전이나 여러 버전을 지정할 수 있습니다. Version Define 표현식은 수학적인 범위 표기법을 사용합니다.

대괄호 “[]”는 범위가 엔드포인트를 포함함을 지정합니다.

[1.3,3.4.1]은 1.3.0 <= x <= 3.4.1로 평가됩니다

괄호 “()”는 범위가 엔드포인트를 제외함을 의미합니다.

(1.3.0,3.4)는 1.3.0 < x < 3.4.0으로 평가됩니다

단일 표현식에서 범위 타입을 혼합할 수 있습니다.

[1.1,3.4)는 1.1.0 <= x < 3.4.0으로 평가됩니다

(0.2.4,5.6.2-preview.2]는 0.2.4 < x <= 5.6.2.-preview.2로 평가됩니다

대괄호 안에 단일 버전 지정자를 사용하여 정확한 버전을 지정할 수 있습니다.

[2.4.5]는 x = 2.4.5로 평가됩니다

좀 더 빠른 방법으로는 범위 대괄호 없이 단일 버전을 입력하여 표현식에 해당 버전 이상이 포함되어 있음을 나타낼 수 있습니다.

2.1.0-preview.7은 x >= 2.1.0-preview.7로 평가됩니다

참고: 표현식에서는 공백이 허용되지 않습니다. 와일드카드 문자는 지원되지 않습니다.

Unity 버전 번호

Unity의 현재 버전(및 어셈블리 정의를 지원하는 모든 버전)은 MAJOR.MINOR.REVISION의 세 부분으로 나뉜 버전 지정자(예: 2017.4.25f1, 2018.4.29f1, 2019.4.7f1)를 사용합니다.

  • MAJOR 버전은 타겟 출시년도입니다(예: 2017년, 2021년).
  • MINOR 버전은 타겟 출시 사분기(예: 1분기, 2분기, 3분기, 4분기)입니다.
  • REVISION 지정자는 세 부분으로 나뉘며, 형식은 RRzNN입니다.
    • RR은 1자리 또는 2자리의 개정 번호입니다.
    • z는 출시 유형을 지정하는 글자입니다.
      • a = 알파 출시
      • b = 베타 출시
      • f = 일반 공개 출시
      • c = 중국 출시 버전(f와 동등)
      • p = 패치 출시
      • x = 실험적 출시
    • NN은 1자리 또는 2자리의 번호로, 나중 출시일수록 커집니다.

출시 유형 지정자의 비교 관계는 다음과 같습니다.

a < b < f = c < p < x

즉, 알파 출시는 베타 출시보다 빠르며, 베타 출시는 일반(f) 출시 또는 중국(c) 출시보다 빠릅니다. 패치 출시는 항상 개정 번호가 같은 일반 출시나 중국 출시보다 늦으며, 실험적 개정 번호는 여타 출시 유형보다 늦습니다. 실험 출시는 끝에 나중 출시일수록 커지는 번호를 사용하지 않습니다.

Unity 버전 번호는 REVISION 컴포넌트 뒤에 2019.3.0f11-Sunflower와 같은 접미사 사용이 허용됩니다. 버전 비교 시 접미사는 무시됩니다.

예를 들어, 다음 표현식은 Unity의 모든 2017 또는 2018 버전을 포함하지만, 2019 이상 버전은 일체 포함하지 않습니다.

[2017,2019)

패키지 및 모듈 버전 번호

패키지 및 모듈 버전 지정자는 MAJOR.MINOR.PATCH-LABEL의 유의적 버전 형식을 따르는 네 부분으로 나뉩니다. 처음 세 부분은 항상 숫자이지만, 레이블은 스트링입니다. 프리뷰 상태의 Unity 패키지는 preview 또는 preview.n이라는 스트링을 사용하며, 여기서 n > 0입니다. 패키지 버전 번호에 대한 자세한 내용은 패키지 버전을 참조하십시오.

예를 들어, 다음 표현식은 MAJOR.MINOR 버전이 3.2 - 6.1(포함)인 모든 패키지 버전을 포함합니다.

[3.2,6.1]

 

스크립트가 속한 어셈블리 찾기

C# 스크립트 중 하나가 컴파일되는 어셈블리를 식별하려면 다음 단계를 따르십시오.

  1. Unity Project 창에서 C# 스크립트 파일을 선택하여 Inspector 창에서 해당 프로퍼티를 확인합니다.
  2. 어셈블리 파일 이름과 어셈블리 정의(존재하는 경우)는 Inspector  Assembly Information 섹션에 표시됩니다.

이 예제에서 선택한 스크립트는 Unity.Timeline.Editor 어셈블리 정의 에셋에서 정의된 라이브러리 파일인 Unity.Timeline.Editor.dll로 컴파일됩니다.

 

특수 폴더

Unity는 폴더 내에서 특정한 특수 이름이 지정된 스크립트를 다른 폴더의 스크립트와 다르게 취급합니다. 단, 이러한 폴더는 해당 폴더 내 또는 상위 폴더에 어셈블리 정의 에셋을 생성하면 더 이상 다르게 취급되지 않습니다. 이와 같은 변화는 Editor 폴더를 사용할 때 가장 잘 알아볼 수 있는데, 이러한 Editor 폴더는 보통 코드 정리 방식 과 사용하는 에셋 스토어 패키지에 따라 프로젝트 전체에 걸쳐 분산되어 있습니다.

보통 Unity는 이름이 Editor인 폴더의 모든 스크립트를 이러한 스크립트의 위치와는 상관없이 사전 정의된 Assembly-CSharp-Editor 어셈블리에 컴파일합니다. 단, 하위 폴더 중 Editor 폴더가 있는 폴더에 어셈블리 정의 에셋을 생성하면 해당 에디터 스크립트가 사전 정의된 에디터 어셈블리가 아닌 어셈블리 정의에 의해 생성된 새로운 어셈블리로 전송됩니다. 이 위치는 잘못된 위치입니다. Editor 폴더를 관리하기 위해 각 Editor 폴더에서 어셈블리 정의 또는 레퍼런스 에셋을 생성하고 해당 스크립트를 하나 이상의 에디터 어셈블리에 배치할 수 있습니다. [에디터 코드용 어셈블리 생성]을 참조하십시오.

 

어셈블리 속성 설정

어셈블리 속성을 사용하여 어셈블리에 대한 메타데이터 프로퍼티를 설정할 수 있습니다. 일반적으로 어셈블리 속성 문은 AssemblyInfo.cs 파일에 저장됩니다.

예를 들어 다음 어셈블리 속성은 몇 가지 .NET 어셈블리 메타데이터 값, InternalsVisibleTo 속성(테스트 작업 시 유용), 그리고 프로젝트를 빌드할 때 미사용 코드를 제거하는 방식에 영향을 주는 Unity 정의 [Preserve 속성]을 지정합니다.

[assembly: System.Reflection.AssemblyCompany("Bee Corp.")]
[assembly: System.Reflection.AssemblyTitle("Bee's Assembly")]
[assembly: System.Reflection.AssemblyCopyright("Copyright 2020.")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("UnitTestAssembly")]
[assembly: UnityEngine.Scripting.Preserve]

 

빌드 스크립트에서 어셈블리 정보 가져오기

UnityEditor.Compilation 네임스페이스에서 CompilationPipeline 클래스를 사용하면 Unity가 프로젝트를 위해 빌드한 모든 어셈블리에 대한 정보를 검색해서 가져올 수 있습니다. 여기에는 어셈블리 정의 에셋에 기반하여 생성된 어셈블리도 포함됩니다.

예를 들어 다음 스크립트는 CompilationPipeline 클래스를 사용하여 프로젝트의 모든 현재 플레이어 어셈블리를 나열합니다.

using UnityEditor;
using UnityEditor.Compilation;
public static class AssemblyLister
{
    [MenuItem("Tools/List Player Assemblies in Console")]
    public static void PrintAssemblyNames()
    {
        UnityEngine.Debug.Log("== Player Assemblies ==");
        Assembly[] playerAssemblies =
            CompilationPipeline.GetAssemblies(AssembliesType.Player);

        foreach (var assembly in playerAssemblies)
        {
            UnityEngine.Debug.Log(assembly.name);
        }
    }
}

C# 클래스나 변수를 선언할때 다음과 같은 접근 제한자가 있다.

  • public : 외부클래스(스크립트)에서 접근가능, 인스펙터에 노출됨
  • private : 동일클래스에서만 접근가능. 외부접근 불가, 인스펙터에 노출안됨
  • protected: private과 동일하게 외부접근 불가능하고 상속받은 파생 클래스에서만 접근가능
  • internal : 같은 어셈블리에서만 접근가능. class의 경우 접근제한자를 생략하면 internal이 기본값으로 설정

액세스 한정자는 멤버 또는 형식의 선언된 접근성을 지정하는데 사용되는 키워드

키워드 접근성
public 제한 없음
protected 포함 및 파생
internal 동일 어셈블리로 제한
protected internal 동일 어셈블리 또는 다른 어셈블리여도 포함하는 클래스에서 파생된 형식
private 포함
private protected 포함 or 동일 어셈블리 내의 포함하는 클래스의 파생 ( C# 7.2 이상 )

* 네임 스페이스에는 액세스 한정자가 허용되지 않음 

public class A { }
// 동일 어셈블리에서는 internal만 사용할 경우 public과 접근 제어 수준이 동일함
internal class B { } 

// protected, protected internal, private, private protected는 namespace에는 허용되지 않기 때문에 에러
protected class C { }

* 멤버 선언이 발생한 구문에 따라 특정 액세스 한정자만 허용

- 멤버가 다른 형식 내에 중첩되면 해당 엑세스 가능 영역은 엑세스 가능성 수준 및 한 수준 위 형식의 엑세스 가능 영역에 의해 결정됨

public class A{
    private class AA {
		public int x;
    }
}

public class B {
    void Test() {
        // AA Class가 private 이기 때문에 AA.x가 public 이여도 외부에서 접근할 수 없음
        A.AA aa = new A.AA();
    }
}

* 기본 멤버 액세스 가능성 및 멤버의 허용된 액세스 가능성

구문 기본 멤버 엑세스 멤버의 허용된 액세스
enum public none
class  private  public / protected / internal / private / protected internal, private protected
interface public public / protected / internal / *private ( 기본 구현 필요 ) / protected internal, private protected
struct private 상속이 안되기 때문에 protected는 제외됨

public / internal / private 

* protected

class A
{
    protected int x = 1;
}

class B : A
{
    void Start()
    {
        A a = new A();
        B b = new B();

        // x에 접근할 수 없기 때문에 에러
        a.x = 1;

        // x에 접근할 수 있기 때문에 정상
        x = 2;
    }
}

* internal

// EveryDayDevup-Framework 어셈블리
// 같은 어셈블리에서는 접근이 가능
internal class A {
    public static int x = 1;
}

class C{
    void Test() {
		A.x = 2;
    }
}


// Assembly-CSharp
class B {
    void Start() {
    	// 어셈블리가 다르기 때문에 A에 접근할 수 없음 
        A.x = 3;
    }
}
// internal class를 public으로 변경하고 멤버를 internal로 변경
public class A
{
	internal static int x = 1;
}

class C
{
	void Test()
    {
		A.x = 2;
    }
}


class B : 
{
    void Start()
    {
        A a = new A();
    	// 인스턴스 생성은 가능하지만
        // internal로 설정한 x에 접근할 수 없음 
        a.x = 2;
    }
 }

* protected internal

// EveryDayDevup-Framework
// 같은 어셈블리에 있는 C는 A.x의 접근할 수 있음
public class A
{
	internal int x = 1;
}

public class C 
{
	void Test()
    {
		x = 3;
    }
}

// Assembly-CSharp
// 다른 어셈블리에 있는 B클래스는 상속을 해도 A.x의 접근할 수 없음
class B : A
{
    void Start()
    {
        x = 2;
    }
}
// EveryDayDevup-Framework
// protected internal을 사용하면 다른 어셈블리의 파생 클래스에서 x의 접근이 가능
// protected internal은 멤버에만 사용할 수 있음
public class A
{
	protected internal int x = 1;
}

// Assembly-CSharp
class B : A
{
    void Start()
    {
     	// protected internal은 상속 시 다른 어셈블리에서 접근이 가능
        x = 2;
    }
}

* private

class A
{
    private int x;
    
    public int GetX() {
    	return x;
    }
}

class B
{
    void Test()
    {
        A a = new A();
        // private는 선언된 클래스 또는 구조체의 본문에서만 접근 가능
        // a.x의 접근할 수 없기 때문에 에러
        a.x;
        
        // A에서 x를 접근할 수 있도록 해줘야함
        a.GetX();
    }
}

* private protected

// EveryDayDevup-Framework
// 동일 어셈블리의 파생 클래스에서는 접근이 가능
public class A {
	private protected int x = 1;
}

public class C : A{
	void Test() {
		x = 2;
    }
}

// Assembly-CSharp
class B : A {
    void Test(){
    	// 다른 어셈블리에서는 상속을 받아도 x에 접근할 수 없음 
        x = 3;
    }
}

 

'유니티게임강좌 > 주인공 캐릭터 제작' 카테고리의 다른 글

[Player제작] 캐릭터 회전 - Rotate  (0) 2023.02.25
Assembly  (0) 2023.02.25
[Player제작] 캐릭터의 이동  (0) 2023.02.25
[Player제작] InputManager  (2) 2023.02.25
[Player제작] 유니티 Class 종류  (0) 2023.02.25

유니티에서 객체를 이동시키거나 회전시키는 방법은 2가지가 있다. 

1. 모든 객체에 있는 Transform컴포넌트의 position, rotation 속성을 지속해서 변경하는 것이다.

2. 내장된 물리엔진을 이용해 물리적인 힘(Force)또는 회전력(Torque)을 가해 변경시키는 것이다. 

 

애니메이션으로도 이동및 회전을 할 수 있지만, 이건 Transform컴포넌트의 속성값을 연속적으로 기록한 것을 재생하는 것이기 때문에  Transform컴포넌트를 이용하는 방법이다.

 

하이라키의 Player Object을 선택한후 인스펙터뷰에서 Add Component에서 new Script를 선택하고 PlayerCtrl로 한다. 

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

public class PlayerCtrl : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");
        transform.position += new Vector3(h, 0f, v);
    }
}

h,v를 입력 받아 Vector3 구조체를 만들어 transform.position에 "+=" 연산해준다.

h,v의 범위는 WASD나 화살표키에 따라 -1f~1f의 범위를 갖기 때문에 Player는 전후좌우로 잘 움직인다.

주의할건 Vector3(h,v,0f)가 아니라 Vector(h,0f,v)라는 거다 유니티는 3차원 좌표시스템인데 y는 위아래를 뜻한다. 따라서 x는 좌우 z는 전후를 뜻한다.

Play해보면 무지하게 빠르지만 잘 움직인다. 왜냐하면 Update()함수는 컴퓨터마다 다르자만 1초에 대략 60번 정도는 호출되기 때문이다. 그리고 이렇게 자주 호출되기때문에 되도록이면 무거운 처리를 피해야한다.

다음과 같이 자주호출될 Transform의 참조를 tr에 저장해놓고 사용하면 좀 가벼워진다. 이걸 컴포넌트캐시라고 한다.

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

public class PlayerCtrl : MonoBehaviour
{
    // Start is called before the first frame update
    Transform tf;
    void Start(){
        tf = GetComponent<Transform>();
    }

    // Update is called once per frame
    void Update(){
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");
        tf.position += new Vector3(h, 0f, v);
    }
}

tf.position += new Vector3(h, 0f, v);를 처리해주는 Translate()라는 함수가 있다

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

public class PlayerCtrl : MonoBehaviour
{
    // Start is called before the first frame update
    Transform tf;
    void Start(){
        tf = GetComponent<Transform>();
    }

    // Update is called once per frame
    void Update(){
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");
        tf.Translate(new Vector3(h, 0f, v));
    }
}

현재 움직임이 너무 빠르다. 왜냐하면 매프레임 호출되기 때문이다. 따라서 움직임을 작게하면 되지만 그렇게 되면 컴퓨터의 성능에 따라 속도차이가 나게 된다. 따라서 매 프레임의 시간간격을 곱해주면 컴퓨터의 성능에 관계없이 움직임 속도가 일정하게 된다. 성능이 나쁜 컴퓨터는 시간간격이 크기 때문에 그만큼 많이 움직이기 때문이다.

d(거리) = v1*t1 = v2*t2 , 컴퓨터 성능에 속도는 비례하지만 프레임간시간간격은 반비례한다.

간단하게 빠른 컴퓨터는 조금 움직이게 해주는거다.

 

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

public class PlayerCtrl : MonoBehaviour
{
    // Start is called before the first frame update
    Transform tf;
    void Start(){
        tf = GetComponent<Transform>();
    }

    // Update is called once per frame
    void Update(){
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");
        tf.Translate(Time.deltaTime * new Vector3(h, 0f, v));
    }
}

단위벡터를 이용해 이동 벡터를 만들수 있다.

Vector3 dir = Vector3.right*x + Vector3.forward*v;

단위벡터와 단위벡터의 합은 √2가 될수 있으므로 스칼라값을 1로 만들기 위해 단위벡터로 만들려면 dir.normalized를 사용해야한다. 

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

public class PlayerCtrl : MonoBehaviour {
    // Start is called before the first frame update
    Transform tf;
    private float speed;
    void Start() {
        tf = GetComponent<Transform>();
        speed = 8f;
    }

    // Update is called once per frame
    void Update() {
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");
        Vector3 dir = Vector3.right* h + Vector3.forward* v;
        tf.Translate(dir.normalized * speed * Time.deltaTime);
    }
}

 

'유니티게임강좌 > 주인공 캐릭터 제작' 카테고리의 다른 글

Assembly  (0) 2023.02.25
[Player제작] 접근제한자  (0) 2023.02.25
[Player제작] InputManager  (2) 2023.02.25
[Player제작] 유니티 Class 종류  (0) 2023.02.25
[Player제작] 스크립트  (0) 2023.02.25

 Input은 외부에서 들어오는 입력값을 관리하는 클래스다. 최근에 추가된 InputSystem도 있지만 다음에 설명하겠다.

유니티는 자주 사용하는 키보드, 마우스, 조이스틱 입력에 대한 조합을 미리 정의해 놓왔고 InputManager에서 관리한다.

입력 관리자

Input Manager 창에서 프로젝트에 대한 입력 축 및 축과 관련된 행동을 정의할 수 있습니다. 액세스하려면 Unity의 메인 메뉴에서 Edit > Project Settings로 이동한 후 오른쪽 내비게이션에서 Input Manager를 선택합니다.

입력 관리자는 다음 타입의 컨트롤을 사용합니다.

  • 는 W 키, Shift 키, 스페이스바 등과 같은 물리적 키보드의 모든 키를 의미합니다.
  • 버튼은 리모콘의 X 버튼처럼 물리적 컨트롤러(예: 게임패드)에 있는 버튼을 가리킵니다.
  • 가상 축(복수형: )은 버튼, 키 등과 같은 컨트롤에 매핑됩니다. 사용자가 컨트롤을 활성화하면 축은 [–1..1] 범위의 값을 수신합니다. 이 값은 스크립트에서 사용할 수 있습니다. Horizontal, Vertical, Fire1, Fire2, Mouse X와 같은 추상적인 개념의 이름으로 정의되어 있다.

 

Input.GetKey("a")와 같이 위에 명시된 명명 규칙을 사용하여 특정 키 또는 버튼에 대한 입력을 쿼리할 수도 있습니다. 예를 들면 다음과 같습니다.

스크립트에서 가상 축 사용
스크립트에서 가상 축에 액세스하기 위해 축 이름을 사용할 수 있습니다.
예를 들어 Horizontal 축의 현재 값을 쿼리하고 변수에 저장하려면 다음과 같이 Input.GetAxis를 사용할 수 있습니다.

Input.GetAxis("Input Manager Axes 이름")으로 값을 얻어 올수 있다.

Input.GetAxis는 -1.0f~1.0f의 변화하는 값을 얻어오는데, 누루고 있으면 0f에서 -1f나 1f로 가속되는 값이 리턴된다.

Input.GetAxisRaw -1.0f, 0f, 1.0f의 3개중 하나의 값이 전달된다.

움직임이 아니라 이벤트(예: 게임 내에서 무기 발사)를 설명하는 축의 경우에는 대신에 Input.GetButtonDown을 사용하십시오.

두 개 이상의 축이 동일한 이름을 사용하는 경우 쿼리는 절대값이 가장 큰 축을 반환합니다. 따라서 축 이름에 두 개 이상의 입력 기기를 할당할 수 있습니다.

예를 들어 Horizontal이라는 이름으로 두 개의 축을 만든 후 하나는 키보드 입력에 할당하고 다른 하나는 조이스틱 입력에 할당할 수 있습니다. 사용자가 조이스틱을 사용하는 경우 입력은 조이스틱에서 수신되고 키보드 입력은 null입니다. 그렇지 않으면 입력은 키보드에서 수신되고 조이스틱 입력은 null입니다. 이를 통해 여러 컨트롤러의 입력을 처리하는 단일 스크립트를 작성할 수 있습니다.

예제

Horizontal  Vertical 축의 입력과 transform.Translate 메서드를 사용하여 XZ 공간(전방, 후방, 왼쪽, 오른쪽)에서 게임 오브젝트를 움직일 수 있습니다. 움직이려는 게임 오브젝트에 연결된 스크립트의 update() 메서드에 다음 코드를 추가하십시오.

float moveSpeed = 10;
//Define the speed at which the object moves.

float horizontalInput = Input.GetAxis("Horizontal");
//Get the value of the Horizontal input axis.

float verticalInput = Input.GetAxis("Vertical");
//Get the value of the Vertical input axis.

transform.Translate(new Vector3(horizontalInput, verticalInput, 0) * moveSpeed * Time.deltaTime);
//Move the object to XYZ coordinates defined as horizontalInput, 0, and verticalInput respectively.

Time.deltaTime은 마지막 프레임 이후 경과한 시간을 나타냅니다. moveSpeed 변수를 Time.deltaTime과 곱하면 게임 오브젝트가 프레임마다 일정한 속도로 움직입니다.

 

Input class는 다음과 같은 함수들을 지원하면 눌렸을때 눌리고 있을때 떨어졌을때를 구별할 수 있습니다.

GetAccelerationEvent Returns specific acceleration measurement which occurred during last frame. (Does not allocate temporary variables).
GetAxis Returns the value of the virtual axis identified by axisName.
GetAxisRaw Returns the value of the virtual axis identified by axisName with no smoothing filtering applied.
GetButton Returns true while the virtual button identified by buttonName is held down.
GetButtonDown Returns true during the frame the user pressed down the virtual button identified by buttonName.
GetButtonUp Returns true the first frame the user releases the virtual button identified by buttonName.
GetJoystickNames Retrieves a list of input device names corresponding to the index of an Axis configured within Input Manager.
GetKey Returns true while the user holds down the key identified by name.
GetKeyDown Returns true during the frame the user starts pressing down the key identified by name.
GetKeyUp Returns true during the frame the user releases the key identified by name.
GetMouseButton Returns whether the given mouse button is held down.
GetMouseButtonDown Returns true during the frame the user pressed the given mouse button.
GetMouseButtonUp Returns true during the frame the user releases the given mouse button.
GetTouch Call Input.GetTouch to obtain a Touch struct.
IsJoystickPreconfigured Determine whether a particular joystick model has been preconfigured by Unity. (Linux-only).
ResetInputAxes Resets all input. After ResetInputAxes all axes return to 0 and all buttons return to 0 for one frame.

+ Recent posts