✅ 오늘의 학습 목표
1. New Input System 학습
2. 농장 게임 만들기 (1)
1. New Input System
여태까지 오브젝트의 움직임을 Input.GetKey() 같은 구 방법으로 구현하였었는데
이제는 New Input System을 활용하여 오브젝트의 움직임을 구현하는 방법에 대해 학습해 보겠다.

- Action Map : 입력의 카테고리 (예시 : Player, UI, Menu)
- Action : 실제 동작(이동, 점프, 공격 등)
- Binding : 액션에 연결된 실제 장치의 버튼/스틱/키
예시: Player 액션맵 안에 Move 액션을 만들고 그 바인딩에 WASD, 조이스틱, VR 컨트롤러 스틱을 한 번에 넣을 수 있음
1. 기본 동작
1.1. Input Actions 생성

1.2. Control Scheme 생성


1.3. Action Properties 설정

[Action]
액션 자체의 기본 성격을 정하는 곳.
- Action Type
- Button → 단순 On/Off 입력 (점프, 공격 같은 거)
- Value → 숫자나 방향 벡터처럼 값을 읽어오는 입력 (이동, 조준)
- Pass Through → 여러 입력 장치 값을 그대로 통과시켜 받을 때 사용 (멀티 터치 같은 특수 상황에 좋음)
- Control Type
- 어떤 형식의 입력값을 받을지 지정
- Button → true/false 형태
- Vector2 → 2D 방향값 (WASD, 조이스틱)
- Vector3 → 3D 방향값 (VR 컨트롤러 위치 등)
- Axis → -1~1 값 (마우스 스크롤, 트리거 압력)
- 어떤 형식의 입력값을 받을지 지정
[Interactions]
입력이 어떤 조건에서 발동할지를 정하는 곳
“버튼을 누르면”이 아니라 “길게 눌러야 발동” 같은 규칙을 여기서 만든다.
- Press
- Press and Release: 누르고 떼면 발동
- Press Only: 누르는 순간 발동
- Release Only: 떼는 순간 발동
- Hold → 일정 시간 이상 누르고 있어야 발동
- Tap → 짧게 눌렀을 때만 발동
- SlowTap → 누르긴 했는데 길게는 아닌 애매한 시간대 발동
- MultiTap → 더블클릭, 트리플탭 같은 거
각 인터랙션마다 Duration(누르고 있는 시간), Press Point(입력 강도 임계값) 같은 세부 옵션이 있음
[Processors]
입력값을 받아서 가공하는 필터 같은 존재
예를 들어 조이스틱이 너무 민감하면 Deadzone을 설정하거나 방향값을 반대로 뒤집을 수 있음.
- Invert → X축, Y축, Z축 값을 반대로
- Clamp → 값 범위를 일정 구간으로 제한
- Normalize → 벡터 길이를 1로 정규화 (방향만 필요할 때)
- Scale → 입력값 크기 조절
- Stick Deadzone → 조이스틱 중앙 근처 작은 움직임 무시
1.4. 바인딩 연결


이제 액션에 연결된 실제 장치의 버튼/스틱/키 입력값을 넣어줄 차례이다.
Path 옆에 있는 Listen 버튼을 누르면 입력 대기 상태가 된다.
이 상태에서 키보드나 마우스를 눌러주면 해당 입력이 자동으로 검색되어 Path에 연결된다.
일일이 리스트에서 찾는 것보다 훨씬 빠르고 편하다.
1.5. 패키지 매니저 기본값 설정

Project Settings > Input System Package로 이동해 Project-wide Actions에 위에서 만든 PlayerAction(Input Action Asset)을 등록한다.
2. 동작 구현 (1)
InputActionAsset을 직접 스크립트에서 불러와서 쓰는 방식

using UnityEngine;
using UnityEngine.InputSystem;
namespace NewInputSystem
{
public class PlayerController1 : MonoBehaviour
{
private CharacterController cc;
private Vector2 moveInput;
public float speed = 5f;
public InputActionAsset inputActionAsset;
private InputAction moveAction;
private InputAction jumpAction;
private InputAction interactionAction;
private InputAction attackAction;
void Start()
{
// Input Action Asset을 스크립트에서 사용
moveAction = InputSystem.actions.FindAction("Move");
jumpAction = InputSystem.actions.FindAction("Jump");
interactionAction = InputSystem.actions.FindAction("Interaction");
attackAction = InputSystem.actions.FindAction("Attack");
cc = GetComponent<CharacterController>();
}
void Update()
{
moveInput = moveAction.ReadValue<Vector2>();
if (moveInput != Vector2.zero)
{
Debug.Log("Move : " + moveAction.ReadValue<Vector2>());
var dir = new Vector3(moveInput.x, 0, moveInput.y).normalized;
cc.Move(dir * speed * Time.deltaTime);
}
if (jumpAction.IsPressed()) // IsPressed() : 여러번 실행 | WasPressedThisFrame() : 한번 실행
Debug.Log("Jump");
if (interactionAction.IsPressed())
Debug.Log("Interaction");
if (attackAction.IsPressed())
Debug.Log("Attack");
}
}
}
- Start()에서 FindAction()으로 각 액션(Move, Jump, Interaction, Attack)을 찾아서 변수에 담는다.
- Update()에서 ReadValue<Vector2>()로 이동 입력을 받고
- IsPressed()나 WasPressedThisFrame()으로 버튼류 액션을 감지한다.
장점은 직관적이고 단순하다는 것.
단점은 FindAction()에 액션 이름을 직접 써야 해서 이름이 바뀌면 코드도 수정해야 한다. (물론 오타 문제도 발생할 수 있음)
또한 액션 활성화/비활성화 제어를 따로 안 하면 오브젝트 비활성 시 입력이 그대로 들어올 수 있다.
3. 동작 구현 (2)
Invoke Unity Events을 쓰는 방식,
CallbackContext를 활용하여 이벤트를 직접 Inspector에 할당하는 방법

using UnityEngine;
using UnityEngine.InputSystem;
namespace NewInputSystem
{
// Invoke 이벤트를 사용하는 방법 1
public class PlayerController2 : MonoBehaviour
{
private CharacterController cc;
private Vector2 moveInput;
public float speed = 5f;
void Start()
{
cc = GetComponent<CharacterController>();
}
// Event 연결 필요
public void Move(InputAction.CallbackContext context)
{
moveInput = context.ReadValue<Vector2>();
if (moveInput != Vector2.zero)
{
Debug.Log("Move : " + moveInput);
var dir = new Vector3(moveInput.x, 0, moveInput.y).normalized;
cc.Move(dir * speed * Time.deltaTime);
}
}
public void Jump(InputAction.CallbackContext context)
{
Debug.Log("Jump");
// 점프 기능 실행
}
}
}
- PlayerInput 컴포넌트에서 이벤트를 인스펙터에 연결해 두면 액션이 호출될 때마다 해당 메서드가 자동 실행된다.
- Move(InputAction.CallbackContext context)처럼 매개변수에서 바로 입력값을 읽어오고
- Jump() 등 다른 액션도 같은 방식으로 처리한다.
이 방식은 Update()에서 매번 값 읽어오는 게 아니라 입력이 발생했을 때만 메서드가 불려서 깔끔하다.
다만 이벤트 연결을 인스펙터에서 일일이 해줘야 하는 번거로움이 있다.
4. 동작 구현 (3)
PlayerInput의 액션들을 CallbackContext를 활용하여 스크립트 상에서 직접 찾아서 이벤트를 등록하는 방법
using UnityEngine;
using UnityEngine.InputSystem;
namespace NewInputSystem
{
// Invoke 이벤트를 사용하는 방법 2 - Event 연결 x
public class PlayerController3 : MonoBehaviour
{
private CharacterController cc;
private Vector2 moveInput;
public float speed = 5f;
private PlayerInput playerInput; // Input Action Asset말고 컴포넌트 통해서 처리 가능
private InputAction moveAction;
private InputAction jumpAction;
void Awake()
{
playerInput = GetComponent<PlayerInput>();
moveAction = playerInput.actions.FindAction("Player/Move");
jumpAction = playerInput.actions.FindAction("Player/Jump");
cc = GetComponent<CharacterController>();
}
// context에 등록? -> 인스펙터 창에서 Evnet 연결 안해도 됨
void OnEnable()
{
moveAction.Enable();
moveAction.performed += MoveStart;
moveAction.canceled += MoveCancel;
jumpAction.Enable();
jumpAction.performed += Jump;
}
void OnDisable()
{
moveAction.Disable();
moveAction.performed -= MoveStart;
moveAction.canceled -= MoveCancel;
jumpAction.Disable();
jumpAction.performed -= Jump;
}
void Update()
{
var dir = new Vector3(moveInput.x, 0, moveInput.y).normalized;
Debug.Log(dir);
cc.Move(dir * speed * Time.deltaTime);
}
// CallbackContext는 키 변경이 있는 경우에만 반환해줌 -> 조건문 걸어서 널 값 체크 안해도 됨
public void MoveStart(InputAction.CallbackContext context)
{
moveInput = context.ReadValue<Vector2>();
}
public void MoveCancel(InputAction.CallbackContext context)
{
moveInput = Vector2.zero;
}
public void Jump(InputAction.CallbackContext context)
{
Debug.Log("Jump");
}
}
}
- playerInput.actions.FindAction("Player/Move")처럼 액션 경로로 찾고 performed·canceled·started 같은 이벤트에 메서드를 바인딩한다.
- 이 방식의 장점은 인스펙터에서 이벤트 연결을 안 해도 된다는 점
- OnEnable/OnDisable에서 액션 활성화와 이벤트 연결·해제를 함께 처리하면 깔끔하게 관리된다.
- CallbackContext는 입력 변화가 있을 때만 값이 들어오기 때문에 null 체크 같은 건 필요 없다.
5. 동작 구현 (4)
Send Messages을 활용하는 방식

using UnityEngine;
using UnityEngine.InputSystem;
namespace NewInputSystem
{
// Input Value(Send Messages)를 사용하는 방법
public class PlayerController4 : MonoBehaviour
{
private CharacterController cc;
public float speed = 5f;
private Vector2 moveInput;
void Start()
{
cc = GetComponent<CharacterController>();
}
void Update()
{
var dir = new Vector3(moveInput.x, 0, moveInput.y);
cc.Move(dir * speed * Time.deltaTime);
}
private void OnMove(InputValue value)
{
moveInput = value.Get<Vector2>();
}
private void OnJump(InputValue value)
{
bool isJump = value.isPressed;
Debug.Log(isJump);
}
// True, False를 활용하기 위해 Input Action Type을 Value로 사용 -> 꾹 누르는 상호작용 가능
private void OnInteraction(InputValue value)
{
Debug.Log(value.isPressed);
}
private void OnAttack(InputValue value)
{
Debug.Log("OnAttack");
}
}
}
- 메서드 이름을 OnMove, OnJump처럼 정해두면 Input System이 알아서 매핑된 액션이 호출될 때 해당 메서드를 실행해 준다.
- 여기서는 InputValue 타입으로 값을 받아 Get<Vector2>()로 방향을 읽거나, isPressed로 버튼 상태를 확인한다.
- Value 타입을 쓰면 버튼을 꾹 누르는 상호작용도 구현 가능하다.
- 장점은 매우 간단하다는 것, 단점은 메서드명이 고정되어 있어 네이밍 변경 자유도가 떨어진다는 것.
2. 농장 게임 구현 (1)
1. Intro 화면 구현

2. Character 선택 화면 구현
🥕 예행 작업
1. Asset 다운로드Creative Characters FREE - Animated Low Poly 3D Models | 3D 휴머노이드 | Unity Asset Store
Elevate your workflow with the Creative Characters FREE - Animated Low Poly 3D Models asset from ithappy. Find this & other 휴머노이드 options on the Unity Asset Store.
assetstore.unity.com
2. Mixamo 다운로드3. Scenes 생성 (Intro, Character, Main)
1. 캐릭터 프리팹 만들기


2. 캐릭터 배치 및 UI 구현

3. 캐릭터 선택 및 애니메이션 구현
버튼을 눌러 캐릭터를 회전시키고 선택하는 기능 구현
[전체 코드]
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
public class SelectCharacter : MonoBehaviour
{
[SerializeField] private Transform centerPivot;
[SerializeField] private Animator[] characterAnims;
[SerializeField] private Button[] turnButtons; // 0 : Left, 1 : Right
[SerializeField] private Button selectButton;
private int currentIndex;
private bool isTurn;
void Start()
{
turnButtons[0].onClick.AddListener(() => Turn(true));
turnButtons[1].onClick.AddListener(() => Turn(false));
selectButton.onClick.AddListener(Select);
}
private void Turn(bool isLeft)
{
if (!isTurn)
{
isTurn = true;
int value = isLeft ? -1 : 1;
currentIndex += value;
// 캐릭터가 4개이기 때문에 0 ~ 3 범위로 설정
if (currentIndex < 0) currentIndex = 3;
else if (currentIndex > 3) currentIndex = 0;
float turnValue = value * 90;
var targetRot = centerPivot.rotation * Quaternion.Euler(0, turnValue, 0);
StartCoroutine(TurnRoutine(targetRot));
}
}
IEnumerator TurnRoutine(Quaternion targetRot)
{
while (true)
{
yield return null; // while true문 사용시 무조건 안에 yield return 필요 (무한 루프 방지)
// 부드럽게 회전
centerPivot.rotation = Quaternion.Slerp(centerPivot.rotation, targetRot, 5f * Time.deltaTime);
var angle = Quaternion.Angle(centerPivot.rotation, targetRot);
if (angle <= 0.1f)
{
isTurn = false;
Debug.Log("Completed Turn");
centerPivot.rotation = targetRot;
yield break;
}
}
}
private void Select()
{
characterAnims[currentIndex].SetTrigger("Select");
Debug.Log($"현재 선택한 캐릭터는 {currentIndex}번째 캐릭터입니다.");
}
}
[버튼 초기화]
turnButtons[0].onClick.AddListener(() => Turn(true));
turnButtons[1].onClick.AddListener(() => Turn(false));
selectButton.onClick.AddListener(Select);
- turnButtons[0] → 왼쪽 화살표 버튼, true 전달
- turnButtons[1] → 오른쪽 화살표 버튼, false 전달
- selectButton → 현재 보고 있는 캐릭터를 선택
[캐릭터 회전 로직]
private void Turn(bool isLeft)
{
if (!isTurn)
{
isTurn = true;
int value = isLeft ? -1 : 1;
currentIndex += value;
if (currentIndex < 0) currentIndex = 3;
else if (currentIndex > 3) currentIndex = 0;
float turnValue = value * 90;
var targetRot = centerPivot.rotation * Quaternion.Euler(0, turnValue, 0);
StartCoroutine(TurnRoutine(targetRot));
}
}
- 중복 회전 방지: isTurn이 false일 때만 회전 시작
- 왼쪽/오른쪽 여부에 따라 currentIndex 조정
- 캐릭터가 4명이므로 인덱스는 0~3 범위에서 순환
- 한 캐릭터당 90도씩 회전 → Quaternion.Euler로 목표 회전 각도 계산
[부드러운 회전 로직]
IEnumerator TurnRoutine(Quaternion targetRot)
{
while (true)
{
yield return null;
centerPivot.rotation = Quaternion.Slerp(centerPivot.rotation, targetRot, 5f * Time.deltaTime);
var angle = Quaternion.Angle(centerPivot.rotation, targetRot);
if (angle <= 0.1f)
{
isTurn = false;
centerPivot.rotation = targetRot;
yield break;
}
}
}
- Slerp로 매 프레임 조금씩 목표 회전 각도로 이동
- 목표와 현재 각도 차이가 0.1도 이하가 되면 회전 종료
- 마지막에 정확히 목표 각도(targetRot)로 맞추고 isTurn을 false로 돌려서 다시 회전 가능 상태로 만든다
[캐릭터 선택]
private void Select()
{
characterAnims[currentIndex].SetTrigger("Select");
Debug.Log($"현재 선택한 캐릭터는 {currentIndex}번째 캐릭터입니다.");
}
- 현재 currentIndex에 해당하는 캐릭터의 애니메이터에 "Select" 트리거 발동
- 선택된 캐릭터 번호를 콘솔에 출력

3. Fade 구현
화면 페이드 인/아웃 기능
[전체 코드]
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
public class Fade : MonoBehaviour
{
private Image fadeImage;
public static Action<float, Color, bool, Action> onFadeAction;
void Awake()
{
fadeImage = GetComponent<Image>();
}
void OnEnable()
{
onFadeAction += OnFade;
}
void OnDisable()
{
onFadeAction -= OnFade;
}
private void OnFade(float t, Color c, bool isFade, Action fadeEvent = null)
{
StartCoroutine(FadeRoutine(t, c, isFade, fadeEvent));
}
IEnumerator FadeRoutine(float fadeTime, Color color, bool isFade, Action fadeEvent = null)
{
fadeImage.raycastTarget = true;
float timer = 0f;
float percent = 0f;
while (percent < 1f)
{
timer += Time.deltaTime;
percent = timer / fadeTime;
float fadeValue = isFade ? percent : 1 - percent;
fadeImage.color = new Color(color.r, color.g, color.b, fadeValue);
yield return null;
}
fadeEvent?.Invoke();
fadeImage.raycastTarget = false;
}
}
[델리게이트 활용]
private Image fadeImage;
public static Action<float, Color, bool, Action> onFadeAction;
void OnEnable() => onFadeAction += OnFade;
void OnDisable() => onFadeAction -= OnFade;
- fadeImage → 실제로 색과 알파값을 변경할 UI 이미지
- onFadeAction → 외부에서 Fade.onFadeAction?.Invoke(...)로 호출 가능
- float t → 페이드에 걸리는 시간
- Color c → 페이드 색상
- bool isFade → true면 페이드 인(점점 보임), false면 페이드 아웃(점점 사라짐)
- Action fadeEvent → 페이드가 끝났을 때 실행할 콜백
[페이드 기능]
private void OnFade(float t, Color c, bool isFade, Action fadeEvent = null)
{
StartCoroutine(FadeRoutine(t, c, isFade, fadeEvent));
}
IEnumerator FadeRoutine(float fadeTime, Color color, bool isFade, Action fadeEvent = null)
{
fadeImage.raycastTarget = true; // 페이드 중 클릭 막기
float timer = 0f;
float percent = 0f;
while (percent < 1f)
{
timer += Time.deltaTime;
percent = timer / fadeTime;
float fadeValue = isFade ? percent : 1 - percent;
fadeImage.color = new Color(color.r, color.g, color.b, fadeValue);
yield return null;
}
fadeEvent?.Invoke();
fadeImage.raycastTarget = false;
}
- 델리게이트로 호출되면 FadeRoutine() 코루틴을 시작해 시간에 따라 화면을 부드럽게 전환
- fadeImage.raycastTarget = true → 페이드 중에는 UI 클릭/터치를 막음
- percent가 1이 될 때까지 루프
- isFade == true → 알파값 0 → 1 (점점 덮기)
- isFade == false → 알파값 1 → 0 (점점 걷기)
- 코루틴 종료 시 fadeEvent 콜백 실행 클릭 막기 해제
[SeletCharacter 스크립트 수정]
private void Select()
{
Debug.Log($"현재 선택한 캐릭터는 {currentIndex}번째 캐릭터입니다.");
StartCoroutine(SelectRoutine());
}
IEnumerator SelectRoutine()
{
characterAnims[currentIndex].SetTrigger("Select");
yield return new WaitForSeconds(3f);
Fade.onFadeAction?.Invoke(3f, Color.white, true, null);
yield return new WaitForSeconds(3.5f);
}
- Select()에서 코루틴을 실행
- 첫 3초는 선택된 캐릭터의 "Select" 트리거 애니메이션 재생
- 그 뒤 Fade.onFadeAction을 호출해서 화면을 3초 동안 흰색으로 덮는 페이드 인
- 콜백은 null로 두었지만 필요하다면 씬 전환 코드를 넣을 수 있다.

4. Main 씬

메인 씬은 우선 오브젝트 배치만 하고 끝이 났다.
using UnityEngine;
using UnityEngine.InputSystem;
namespace Farm
{
public class PlayerController : MonoBehaviour
{
private Animator anim;
private PlayerInput playerInput;
private CharacterController cc;
private Vector3 moveInput;
private float moveSpeed = 2f;
private float turnSpeed = 10f;
void Start()
{
anim = GetComponent<Animator>();
cc = GetComponent<CharacterController>();
}
void Update()
{
cc.Move(moveInput * moveSpeed * Time.deltaTime);
Turn();
}
void OnMove(InputValue value)
{
var move = value.Get<Vector2>();
moveInput = new Vector3(move.x, 0, move.y);
}
private void Turn()
{
if (moveInput != Vector3.zero)
{
// LookAt = Transform 기반의 특정 Vector3를 바라보는 기능
// LookRotation = Quaternion 기반의 특정 Vector3를 바라보는 기능
Quaternion targetRot = Quaternion.LookRotation(moveInput);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRot, turnSpeed * Time.deltaTime); // Slerp(현재 회전, 목표 회전, 비율(속도))
}
}
}
}
플레이어 움직임은 앞에서 배운 Player Input을 활용해 주었다.

참고로 언팩 작업은 프리팹 우클릭 후 처리할 수 있다.
'Unity > 멋쟁이사자처럼 부트캠프' 카테고리의 다른 글
| [멋쟁이사자처럼부트캠프] 유니티 게임 개발 5기(61일차) - 농장 게임 (3) (7) | 2025.08.13 |
|---|---|
| [멋쟁이사자처럼부트캠프] 유니티 게임 개발 5기(60일차) - 농장 게임 (2) (6) | 2025.08.12 |
| [멋쟁이사자처럼부트캠프] 유니티 게임 개발 5기(58일차) - 데이터 파싱 (2), API 활용 (9) | 2025.08.10 |
| [멋쟁이사자처럼부트캠프] 유니티 게임 개발 5기(57일차) - 게임 디자인 패턴 (2), ScriptableObject, 데이터 파싱 (6) | 2025.08.07 |
| [멋쟁이사자처럼부트캠프] 유니티 게임 개발 5기(56일차) - 게임 디자인 패턴 (1) (9) | 2025.08.06 |
