본문 바로가기
Unity_Learn

유니티런 Zombie게임 만들기 1탄, Lighting 굽기 및 스크립터블 오브젝트

by unizzangbd 2025. 7. 24.


3/19_Zombie

1. Lighting굽기

  • New Lighting Settings 클릭 하고 이름을 Main Light Settings로 변경
  • 환경광(Environment Lighting) 설정 → color로 변경
갈색으로 컬러 변경
  • 다시 Scene으로 돌아와서 Global Illumination(글로벌 일루미네이션) 체크
실시간 글로벌 일루미네이션, 리얼타임 체크
베이크드 일루미네이션 체크
수치 조절
  • 구워지면 이렇게 우중충

2. 캐릭터 애니메이션 구성하기

  • 프리팹에서 Women 꺼내서 하이어라키에 놓고 이름 바꾸기
  • 리지드바디 추가 후 수치 변경
  • 캡슐 콜라이더 추가 후 수치 변경
  • 오디오소스 추가
  • 애니메이터 할당하기

3. 애니메이터 구경

팔다리 동작이 다르기에 나뉘어있아, 위에서 아래로 덮어쓰기됨, 그래서 베이스가 기본으로 깔림
- 는 뒤로 아닌건 앞으로
  • Idle에서 Reload는 동작 끝날때 까지 기다릴 필요없지만 장전은 기다려야해서 체크
  • 아바타마스크
적용할 부위만 초록색
  • Upper Body 레이어에 Upper Body Mask 적용

4. Player Input, Player Movement 스크립트 작성

  • 입력을 감지하는 스크립트와 움직임을 따로 작성(입력과 액터 나누기)
둘 다 플레이어에게 할당

4-1. Player Input 스크립트

using UnityEngine;

// 플레이어 캐릭터를 조작하기 위한 사용자 입력을 감지
// 감지된 입력값을 다른 컴포넌트들이 사용할 수 있도록 제공
public class PlayerInput : MonoBehaviour {
    public string moveAxisName = "Vertical"; // 앞뒤 움직임을 위한 입력축 이름
    public string rotateAxisName = "Horizontal"; // 좌우 회전을 위한 입력축 이름
    public string fireButtonName = "Fire1"; // 발사를 위한 입력 버튼 이름
    public string reloadButtonName = "Reload"; // 재장전을 위한 입력 버튼 이름

    // 값 할당은 내부에서만 가능
    public float move { get; private set; } // 감지된 움직임 입력값
    public float rotate { get; private set; } // 감지된 회전 입력값
    public bool fire { get; private set; } // 감지된 발사 입력값
    public bool reload { get; private set; } // 감지된 재장전 입력값

    // 매프레임 사용자 입력을 감지
    private void Update() {
        // 게임오버 상태에서는 사용자 입력을 감지하지 않는다
        if (GameManager.instance != null && GameManager.instance.isGameover)
        {
            move = 0;
            rotate = 0;
            fire = false;
            reload = false;
            return;
        }

        // move에 관한 입력 감지
        move = Input.GetAxis(moveAxisName);
        // rotate에 관한 입력 감지
        rotate = Input.GetAxis(rotateAxisName);
        // fire에 관한 입력 감지
        fire = Input.GetButton(fireButtonName);
        // reload에 관한 입력 감지
        reload = Input.GetButtonDown(reloadButtonName);
    }
}
  • 앞, 뒤, 좌, 우 : 축으로 감지 → 숫자로 반환
  • 발사, 장전 : 버튼으로 감지 → true, false로 반환

4-2. Player Moverment 스크립트

using UnityEngine;

// 플레이어 캐릭터를 사용자 입력에 따라 움직이는 스크립트
public class PlayerMovement : MonoBehaviour 
{
    public float moveSpeed = 5f; // 앞뒤 움직임의 속도
    public float rotateSpeed = 180f; // 좌우 회전 속도


    private PlayerInput playerInput; // 플레이어 입력을 알려주는 컴포넌트
    private Rigidbody playerRigidbody; // 플레이어 캐릭터의 리지드바디
    private Animator playerAnimator; // 플레이어 캐릭터의 애니메이터

    private void Start() 
    {
        // 사용할 컴포넌트들의 참조를 가져오기
        playerInput = GetComponent<PlayerInput>();
        playerRigidbody = GetComponent<Rigidbody>();
        playerAnimator = GetComponent<Animator>();
    }

    // FixedUpdate는 물리 갱신 주기에 맞춰 실행됨(기본값 0.02초)
    private void FixedUpdate() 
    {
        // 물리 갱신 주기마다 움직임, 회전, 애니메이션 처리 실행
        // 회전 실행
        Rotate();
        // 움직임 실행
        Move();

        // 입력 값에 따라 애니메이터의 Move 파라미터 값 변경
        playerAnimator.SetFloat("Move", playerInput.move); // 입력에 따라 애니메이션 변경됨 걷고 뛰고 

    }

    // 입력값에 따라 캐릭터를 앞뒤로 움직임
    private void Move() 
    {
        // 상대적으로 이동할 거리 계산 
        Vector3 moveDistance = playerInput.move * transform.forward * moveSpeed * Time.deltaTime;
        // 리지드바디를 이용해 게임 오브젝트 위치 변경
        playerRigidbody.MovePosition(playerRigidbody.position + moveDistance);
    }

    // 입력값에 따라 캐릭터를 좌우로 회전
    private void Rotate() 
    {
        // 상대적으로 회전할 수치 계산
        float turn = playerInput.rotate * rotateSpeed * Time.deltaTime;
        // 리지드바디를 이용해 게임 오브젝트 회전 변경
        playerRigidbody.rotation = playerRigidbody.rotation * Quaternion.Euler(0, turn, 0f);
    }
}
  • SetFloat("변수", 어떤 움직임) : 애니메이터 변수를 변경하는 내장메서드 : move 애니메이터 내의 Move라는 변수를 playerInput.move으로 변경
  • 이동할 거리 : 방향 * 속력 * 시간 : 이동할 거리에 PlayerInput의 입력값이 곱해지면 움직이는 방향이 조절됨 : 입력값 1 → 앞으로 이동 / 0 → 안움직임 / -1 → 뒤로 이동
  • Quaternion.Euler(x, y, z) : Quaternion.Euler(0, turn, 0f) → y축 회전을 turn수치만큼 : 1입력받으면 오른쪽 회전 -1 입력받으면 왼쪽 회전
  • MovePosition( ) : 리지드바디 관련 내장 메서드 : transform.position = transform.position+ movementDistance 할 수도 있지만 이렇게 하면 물리처리를 무시하기 때문에 벽을 통과해 다닌다. 그래서 리지드바디 관련 MovePosition을 씀 : 상대위치가 아닌 절대위치를 변경하기 때문에 playerRigidbody.position + moveDistance 이렇게 현재위치 + 상대적으로 더 이동할 거리로 씀

5. 시네머신 추적 카메라 구성하기

  • 카메라 움직임을 손쉽게 제어하는 유니티 공식 패키지

5-1. 메인카메라에 CinemachineBrain 컴포넌트 추가

  • 메인이 브레인이 되며 이외의 가상카메라(Virtual Camera) 여럿을 관리하게 한다
  • 가상카메라는 메인카메라의 분신이며, 진짜 카메라는 결국 메인카메라다
  • 메인카메라 = 뇌, 머리 , 가상카메라 = 여러 몸 분신. → 분신에 뇌가 깃들 수 있다, 설정값도 가져올 수 있다

5-2. 하이어라키에서 Cinemachine → Virtual Camera 추가

  • Follow 와 Look At에 플레이어 캐릭터 할당
  • 데드존, 소프트존, 하드리밋 설정 - 데드존 : 화면의 하얀부분 : 카메라가 주시하는 물체가 데드존에 있으면 카메라의 에임이 맞는 것(카메라가 회전하지 않음) - 소프트존 : 화면의 하늘색 부분 : 물체가 데드존을 벗어나 소프트 존에 있다면 카메라가 부드럽게 회전해 에임에 오도록 맞춘다 - 데드존 : 화면의 빨간 부분 : 물체가 데드존에 위치하면 많이 벗어났으니 카메라가 급격히 회전해 에임에 오게 맞춘다
  • 가상 카메라의 파라미터 조절
  • FOV : 시야각(좁을수록 줌인)
  • Body - 카메라가 Follow에 할당된 추적 대상을 어떻게 따라나닐지 결정 - 추적대상 = 카메라의 몸 : 카메라 = 머리 : 카메라는 World Space(전역공간) 을 기준으로 (-8, 16, -8) 떨어져서 몸을 따라다닌다 - Damping : 제동값, 급격한 변화를 ‘꺽어’ 이전값 이후값을 부드럽게 이어주는 비율 : 값이 커질수록 카메라 위치의 급격한 변화가 줄어들지만 신속하게 변경되지 않고 지연시간이 늘어난다.
  • Aim 파라미터: 카메라 조준점
  • Tracked Object Offset : 얼마나 떨어진 곳을 조준할 지 : 캐릭터 실제위치보다 Y축으로 0.5 높은 곳
  • 제동값 다 0 으로 하고 소프트존도 없애서 카메라가 지연시간 없이 플레이어 캐릭터를 조준하도록 만듦

6. 인터페이스

  • 인터페이스란 외부와 통신하는 공개 통로, 통로의 구격 (마치 like USB)
  • 클래스가 반드시 구현해야하는 메서드(기능)들의 틀을 정의하는 개념 → 인터페이스를 상속받은 클래스는 무조건 인터페이스의 메서드를 구현 해야함
  • C#에서 interface 키워드를 사용해서 만들 수 있다
  • 느슨한 커플링 : 어떤 코드가 특정 클래스의 구현에 결합되지 않아 유연하게 변경 가능한 상태

EX) IItem 스크립트( Item앞에 붙은 I가 인터페이스 스크립트인걸 암시)

using UnityEngine;

// 아이템 타입들이 반드시 구현해야하는 인터페이스
public interface IItem 
{
    // 입력으로 받는 target은 아이템 효과가 적용될 대상
    void Use(GameObject target);
}
  • 선언만 있다

EX) IItem을 상속하는 AmmoPack 스크립트

using UnityEngine;

// 총알을 충전하는 아이템
public class AmmoPack : MonoBehaviour, IItem {
    public int ammo = 30; // 충전할 총알 수

    public void Use(GameObject target) {
        // 전달 받은 게임 오브젝트로부터 PlayerShooter 컴포넌트를 가져오기 시도
        PlayerShooter playerShooter = target.GetComponent<PlayerShooter>();

        // PlayerShooter 컴포넌트가 있으며, 총 오브젝트가 존재하면
        if (playerShooter != null && playerShooter.gun != null)
        {
            // 총의 남은 탄환 수를 ammo 만큼 더한다
            playerShooter.gun.ammoRemain += ammo;
        }

        // 사용되었으므로, 자신을 파괴
        Destroy(gameObject);
    }
}
  • public void Use(GameObject target) 메서드를 구현한걸 볼 수 있다

Ex) IDamagerable 스크립트 (인터페이스)

using UnityEngine;

// 데미지를 입을 수 있는 타입들이 공통적으로 가져야 하는 인터페이스
public interface IDamageable 
{
    // 데미지를 입을 수 있는 타입들은 IDamageable을 상속하고 OnDamage 메서드를 반드시 구현해야 한다
    // OnDamage 메서드는 입력으로 데미지 크기(damage), 맞은 지점(hitPoint), 맞은 표면의 방향(hitNormal)을 받는다
    void OnDamage(float damage, Vector3 hitPoint, Vector3 hitNormal);
}
  • 공격당할 수 있는 모든 대상을 위한 인터페이스

7. 캐릭터에 우클릭 빈 GameObject를 만들고 이름을 Gun Pivot으로 설정

  • 총을 배치할 지점을 만드는 것

7-1. 프리팹에 Gun을 꺼내 Gun Pivot에 넣는다

7-2. Gun에 Line Renderer 컴포넌트 추가

  • 렌더러는 일단 꺼둔다
분홍색이 렌더러
포지션 사이즈 조절, 넓이 조절, bullet 할당, 쉐도우 off
리씨브 쉐도우도 끄기

7-3. 오디오 소스 추가

플레이 온 어웨이크 해제

7-4. 파티클 프리팹을 Gun에 넣어주기

8. 스크립터블 오브젝트

  • 여러 오브젝트가 사용할 데이터를 유니티 에셋 형태로 저장할 수 있는 타입

Ex) GunData 스크립트 (총의 수치에 대해 따로 작성한 스크립트)

using UnityEngine;

[CreateAssetMenu(menuName = "Scriptable/GunData", fileName = "Gun Data")]

public class GunData : ScriptableObject
{
    public AudioClip shotClip; // 발사 소리
    public AudioClip reloadClip; // 재장전 소리

    public float damage = 25; // 공격력

    public int startAmmoRemain = 100; // 처음에 주어질 전체 탄약
    public int magCapacity = 25; // 탄창 용량

    public float timeBetFire = 0.12f; // 총알 발사 간격
    public float reloadTime = 1.8f; // 재장전 소요 시간
}
  • [CreateAssetMenu]를 선언 해둬야 스크립터블 오브젝트 사용 가능
  • 총의 업그레이드나 다른 종류의 총이 등장해도 수정하기 편하고 관리하기 쉬워진다
  • 게임오브젝트가 아닌 형태로 존재 해야한다 → 게임오브젝트는 씬에서만 수정 가능하기에

9. Gun 스크립트 작성

using System.Collections;
using UnityEngine;

// 총을 구현
public class Gun : MonoBehaviour
{
    // 총의 상태를 표현하는 데 사용할 타입을 선언
    public enum State
    {
        Ready, // 발사 준비됨
        Empty, // 탄알집이 빔
        Reloading // 재장전 중
    }

    public State state { get; private set; } // 현재 총의 상태

    public Transform fireTransform; // 탄알이 발사될 위치

    public ParticleSystem muzzleFlashEffect; // 총구 화염 효과
    public ParticleSystem shellEjectEffect; // 탄피 배출 효과

    private LineRenderer bulletLineRenderer; // 탄알 궤적을 그리기 위한 렌더러

    private AudioSource gunAudioPlayer; // 총 소리 재생기

    public GunData gunData; // 총의 현재 데이터

    private float fireDistance = 50f; // 사정거리

    public int ammoRemain = 100; // 남은 전체 탄알
    public int magAmmo; // 현재 탄알집에 남아 있는 탄알

    private float lastFireTime; // 총을 마지막으로 발사한 시점
  • enum 열거형으로 총이 어떤 상태인지 저장하는 * 상태(State)*를 만듦
   private void Awake()
    {
        // 사용할 컴포넌트의 참조 가져오기
        gunAudioPlayer = GetComponent<AudioSource>();
        bulletLineRenderer = GetComponent<LineRenderer>();

        // 사용할 점을 두개로 변경
        bulletLineRenderer.positionCount = 2;
        // 라인 렌더러를 비활성화
        bulletLineRenderer.enabled = false;
    }

    private void OnEnable()
    {
        // 총 상태 초기화
        // 전체 예비 탄알 양을 초기화
        ammoRemain = gunData.startAmmoRemain;
        // 현재 탄창을 가득 채우기
        magAmmo = gunData.magCapacity;
        // 총의 현재 상태를 총을 쏠 준비가 된 상태로 변경
        state = State.Ready;
        // 마지막으로 총을 쏜 시점을 초기화
        lastFireTime = 0;
    }

    // 발사 시도
    public void Fire()
    {
        // 현재 상태가 발사 가능한 상태 
        // && 마지막 총 발사 시점에서 gunData.timeBetFire 이상의 시간이 지남
        if(state == State.Ready && Time.time >= lastFireTime + gunData.timeBetFire)
        {
            // 마지막 총 발사 시점 갱신
            lastFireTime = Time.time;
            // 실제 발사 처리 실행
            Shot();
        }
    }

    // 실제 발사 처리
    private void Shot()
    {
        // 레이캐스트에 의한 충돌 정보를 저장하는 컨테이너 
        RaycastHit hit;
        // 탄알이 맞은 곳을 저장할 변수
        Vector3 hitPosition = Vector3.zero;
        // 레이캐스트(시작 지점, 방향, 충돌정보 컨테이너, 사정거리)
        if (Physics.Raycast(fireTransform.position, fireTransform.forward, out hit, fireDistance))
        {
            // 레이가 어떤 물체와 충돌한 경우

            // 충돌한 상대방으로부터 IDamageable 오브젝트 가져오기 시도
            IDamageable target = hit.collider.GetComponent<IDamageable>();
            // 상대방으로부터 IDamageable 오브젝트를 가져오는 데 성공했다면
            if (target != null)
            {
                // 상대방의 OnDamage 함수를 실행시켜 상대방에 데미지 주기
                target.OnDamage(gunData.damage, hit.point, hit.normal);
            }

            // 레이가 충돌한 위치 저장
            hitPosition = hit.point;
        }
        else
        {
            // 레이가 다른 물체와 충돌하지 않았다면
            // 탄알이 최대 사정거리까지 날아갔을 때의 위치를 충돌 위치로 사용
            hitPosition = fireTransform.position + fireTransform.forward * fireDistance;
        }
  • State.Ready : 준비 된 상태
  • 현재 시간 > = 마지막 발사시점(최근 발사시점) + 발사 간격
  • Raycast
  • IDamageable 타입의 타겟이 존재한다면 → if (target != null)
  • hitPoint : 맞은 위치, hit.Normal : 탄알이 충돌한 표면의 방향
// 발사 이펙트 재생 시작(코루틴)
        StartCoroutine(ShotEffect(hitPosition));

        // 남은 탄알 수를 -1
        magAmmo--;

        if (magAmmo <= 0)
        {
            // 탄창에 남은 탄알이 없다면 총의 현재 상태를 Empty로 갱신
            state = State.Empty;
        }


    }

    // 발사 이펙트와 소리를 재생하고 탄알 궤적을 그림
    private IEnumerator ShotEffect(Vector3 hitPosition)
    {
        // 총구 화염 효과 재생
        muzzleFlashEffect.Play();
        // 탄피 배출 효과 재생
        shellEjectEffect.Play();
        // 총격 소리 재생
        gunAudioPlayer.PlayOneShot(gunData.shotClip);
        // 선의 시작점은 총구의 위치
        bulletLineRenderer.SetPosition(0, fireTransform.position);
        // 선의 끝점은 입력으로 들어온 충돌 위치 
        bulletLineRenderer.SetPosition(1, hitPosition);
        // 라인 렌더러를 활성화하여 탄알 궤적을 그림
        bulletLineRenderer.enabled = true;

        // 0.03초 동안 잠시 처리를 대기
        yield return new WaitForSeconds(0.03f);

        // 라인 렌더러를 비활성화하여 탄알 궤적을 지움
        bulletLineRenderer.enabled = false;
    }

    // 재장전 시도
    public bool Reload()
    {
        // 이미 재장전 중이거나 남은 탄알이 없거나
        // 탄창에 찬알이 이미 가득한 경우
        if (state == State.Reloading || ammoRemain <= 0 || magAmmo >= gunData.magCapacity)
        {
            // 재장전 할 수 없음
            return false;
        }

        // 재장전 처리 시작(코루틴)
        StartCoroutine(ReloadRoutine());
        return false;
    }

    // 실제 재장전 처리를 진행
    private IEnumerator ReloadRoutine()
    {
        // 현재 상태를 재장전 중 상태로 전환
        state = State.Reloading;

        // 재장전 소리 재생
        gunAudioPlayer.PlayOneShot(gunData.reloadClip);

        // 재장전 소요 시간 만큼 처리 쉬기
        yield return new WaitForSeconds(gunData.reloadTime);

        // 탄창에 채울 탄알 계산
        int ammoToFill = gunData.magCapacity - magAmmo;

        // 탄창에 채워야 할 탄알이 남은 탄알보다 많다면
        if(ammoRemain < ammoToFill)
        {
            // 채워야 할 탄알 수를 남은 탄알 수에 맞춰 줄임
            ammoToFill = ammoRemain;
        }

        // 탄창을 채움
        magAmmo += ammoToFill;
        // 남은 탄알에서 탄창에 채운만큼 탄알을 뺌 
        ammoRemain -= ammoToFill;
        // 총의 현재 상태를 발사 준비된 상태로 변경
        state = State.Ready;
    }
}
  • bulletRenderer 점 2개 선언한걸 지금 하나는 총구에, 하나는 힛포지션(맞는곳)에 배치.
  • 코루틴 시작(발사이펙트)
  • ammoToFill : 채워넣어야 할 탄알 수 = 탄창최대용량 - 현재 탄알 수

10. 스크립트 할당하기

11. 플레이어 슈터 만들기

  • 플레이어 입력에 따라 총을 쏘거나 재장전
  • 플레이어의 손이 항상 총의 손잡이에 있게 하기
using UnityEngine;

// 주어진 Gun 오브젝트를 쏘거나 재장전
// 알맞은 애니메이션을 재생하고 IK를 사용해 캐릭터 양손이 총에 위치하도록 조정
public class PlayerShooter : MonoBehaviour
{
    public Gun gun; // 사용할 총
    public Transform gunPivot; // 총 배치의 기준점
    public Transform leftHandMount; // 총의 왼쪽 손잡이, 왼손이 위치할 지점
    public Transform rightHandMount; // 총의 오른쪽 손잡이, 오른손이 위치할 지점

    private PlayerInput playerInput; // 플레이어의 입력
    private Animator playerAnimator; // 애니메이터 컴포넌트

    private void Start()
    {
        // 사용할 컴포넌트들을 가져오기
        playerInput = GetComponent<PlayerInput>();
        playerAnimator = GetComponent<Animator>();
    }

    private void OnEnable()
    {
        // 슈터가 활성화될 때 총도 함께 활성화
        gun.gameObject.SetActive(true);
    }

    private void OnDisable()
    {
        // 슈터가 비활성화될 때 총도 함께 비활성화
        gun.gameObject.SetActive(false);
    }​

 

  • OnEnable( ) : 게임 오브젝트가 활성화 될 때 자동으로 호출 되는 메서드 , 총도 함께 활성화!
  • OnDisable( ) : 게임 오브젝트가 비활성화 될 때 자동으로 호출
private void Update()
    {
        // 입력을 감지하고 총 발사하거나 재장전
        
        // 발사 입력 감지 시 
        if (playerInput.fire)
        {
            // 총 발사
            gun.Fire();
        }
        // 재장전 입력 감지 시 
        else if(playerInput.reload)
        {
            // 재장전
            if(gun.Reload())
            {
                // 재장전 성공 시에만 재장전 애니메이션 재생
                playerAnimator.SetTrigger("Reload");
            }
        }
        // 남은 탄알 UI 갱신
        UpdateUI();
    }

    // 탄약 UI 갱신
    private void UpdateUI()
    {
        if (gun != null && UIManager.instance != null)
        {
            // UI 매니저의 탄약 텍스트에 탄창의 탄약과 남은 전체 탄약을 표시
            UIManager.instance.UpdateAmmoText(gun.magAmmo, gun.ammoRemain);
        }
    }

    // 애니메이터의 IK 갱신
    private void OnAnimatorIK(int layerIndex)
    {
        // 총의 기준점 gunPivot을 3D 모델의 오른쪽 팔꿈치 위치로 이동
        gunPivot.position = playerAnimator.GetIKHintPosition(AvatarIKHint.RightElbow);
        // GetIKHintPosition : 캐릭터의 특정 부위 위치를 알려주는 기능

        // IK를 사용하여 왼손의 "위치"와 "회전"을 총의 왼쪽 손잡이에 맞춤
        playerAnimator.SetIKPositionWeight(AvatarIKGoal.LeftHand, 1.0f);
        playerAnimator.SetIKRotationWeight(AvatarIKGoal.LeftHand, 1.0f);

        playerAnimator.SetIKPosition(AvatarIKGoal.LeftHand, leftHandMount.position);
        playerAnimator.SetIKRotation(AvatarIKGoal.LeftHand, leftHandMount.rotation);
         // SetIKPosition : IK 목표의 위치를 설정하는 역할 
 // SetIKPositionWeight : IK를 제어하는데, 0과 1 사이의 가중치 값을 설정하여 영향력을 조절

        // IK를 사용하여 오른손의 위치와 회전을 총의 오른손 손잡이에 맞춤
        playerAnimator.SetIKPositionWeight(AvatarIKGoal.RightHand, 1.0f);
        playerAnimator.SetIKRotationWeight(AvatarIKGoal.RightHand, 1.0f);

        playerAnimator.SetIKPosition(AvatarIKGoal.RightHand, rightHandMount.position);
        playerAnimator.SetIKRotation(AvatarIKGoal.RightHand, rightHandMount.rotation);
    }
}
  • GetIKHintPosition : 캐릭터의 특정 부위 위치를 알려주는 기능
  • SetIKPositionWeight( ) : 얼마나 IK의 영향을 받을지 정하는 메서드
  • SetIKPosition : IK 목표의 위치를 설정하는 역할

12. 스크립트에 해당 항목 할당