2D 애니메이션
2D 게임에서 입력에 따라 캐릭터의 애니메이션을 적용하는 방법은 다음과 같다.
우선, 애니메이션을 적용할 2D 이미지를 준비한다.
이후, Sprite Editor를 통해 다음과 같이 Slice 해주면 된다.
자동으로 크기를 설정해도 되고 원하는 크기로 자를 수도 있다.
Slice가 적용되면 다음과 같다.
Slice가 완료되면 다음과 같이 분할된 스프라이트가 생성된다.
캐릭터로 사용할 GameObject를 생성하고 Sprite Render와 Animator를 붙여준다.
원하는 애니메이션을 Animation View에 넣고 SampleRate를 설정하면 애니메이션을 만들 수 있다.
만들어진 애니메이션을 Animator에서 설정하면 다음과 같이 동작한다.
트랜지션 조절하면 애니메이션 간의 전이를 할 수 있다.
하지만, 트랜지션마다 변수가 다르고 제어하기 쉽지 않기 때문에 다른 방법을 활용하는 것을 권장한다.
InputSystem
InputSystem을 사용하기 위해 우선 Import를 진행한 후, Player의 입력을 제어하기 위해 다음과 같이 설정하자.
이제, InputAction을 만들고 Scheme을 생성하자.
Device Type을 통해 어느 기기에 대한 입력을 처리할 것인지 설정한다.
지금은 Keyboard로 제어할 것이기 때문에 Keyboard로 설정하였다.
이후, ActionMap을 만들면 된다.
ActionMap에 Action을 추가해 보자.
먼저 Action을 생성하고 Action에 맞게 Type을 설정하자.
캐릭터 이동에 대한 입력을 처리할 것이기 때문에 Type을 Value로 변경하고 ControlType을 Vector2D로 변경하자.
이후, up, down, left, right에 대한 입력을 처리할 수 있도록 바인딩을 추가하자.
이후, 방향에 맞게 키를 설정하면 된다.
설정이 완료되었다면 저장한 뒤, C#클래스를 생성하자.
생성된 C#클래스를 캐릭터에 추가하고 어떠한 InputScheme을 사용할지 설정하자.
Behavior 옵션 중 Send Message, Broadcast Message는 성능이 좋지 않고, Invoke Unity Events는 제어하기 쉽지 않기 때문에 Invoke C Sharp Events를 사용하는 것을 권장한다.
지금까지의 작업은 입력을 감지하고 이를 원하는 값으로 변환해 주는 작업이다.
이제, 변환된 값을 통해 캐릭터가 어떻게 동작할지 제어하는 스크립트를 작성하자.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class CharacterController : MonoBehaviour
{
[Header("Move")]
private static readonly int SpeedId = Animator.StringToHash("Speed");
[SerializeField] private float Speed = 5.0f;
[SerializeField] private Camera _mainCamera;
[Header("Component Cache")]
private InputAction Move_Input;
private Animator _animator;
private Rigidbody2D _rigidbody;
private SpriteRenderer _spriteRenderer;
[Header("Camera")]
private Vector3 cameraOffset;
[SerializeField] private float cameraSpeed = 4f;
[SerializeField] private float maxDistance = 4f;
void Start()
{
_animator = GetComponent<Animator>();
_rigidbody = GetComponent<Rigidbody2D>();
_spriteRenderer = GetComponent<SpriteRenderer>();
UnityEngine.InputSystem.PlayerInput Input = GetComponent<UnityEngine.InputSystem.PlayerInput>();
Move_Input = Input.actions["Move"];
cameraOffset = _mainCamera.transform.position - transform.position;
}
void Update()
{
Vector2 moveValue = Move_Input.ReadValue<Vector2>();
if (moveValue.x != 0) _spriteRenderer.flipX = moveValue.x < 0;
_animator.SetFloat(SpeedId, Mathf.Abs(moveValue.x));
transform.position += new Vector3(moveValue.x * Speed, 0, 0) * Time.deltaTime;
}
private void LateUpdate()
{
var currentCamPos = _mainCamera.transform.position;
var characterPos = transform.position;
var newPos = Vector3.MoveTowards(currentCamPos, characterPos + cameraOffset, cameraSpeed * Time.deltaTime);
var distance = Vector3.Distance(characterPos, newPos);
if (distance > maxDistance)
{
newPos = characterPos + (newPos - characterPos) * maxDistance / distance;
}
_mainCamera.transform.position = newPos;
}
}
LateUpdate는 모든 Update가 완료된 이후 실행되는 함수이다.
따라서, 카메라 팔로우 등 천천히 실행돼도 괜찮은 작업을 하기에 적합하다.
Tile Map
타일맵을 생성하는 방법을 알아보자.
Window를 열어 Tile Palette를 열어 새로운 Palette를 생성하자.
그리고 원하는 Texture를 드래그 앤 드롭하면 다음과 같이 된다.
그리고 Rectable을 추가한다.
Rectable은 타일을 그릴 수 있는 도화지 같은 것이다.
이후, Tile Palette에서 원하는 타일을 골라 Paint Brush를 켜주고 원하는 곳을 채워 넣으면 된다.
해당 타일을 땅으로 활용할 것이므로 캐릭터가 충돌해야 한다.
따라서, Collider와 RigidBody를 추가하자.
지금 Collider가 Tile마다 걸려있어 성능상 좋지 않다.
타일의 외면에만 충돌하면 되기 때문에 이를 하나로 합쳐주는 Composite Collider 2D 컴포넌트를 추가하고 Collider2D에서 Composite옵션을 켜주자.
그리고 각 Tile은 타일 전체를 기준으로 충돌을 적용할지, Sprite를 기준으로 충돌을 적용할지 지정할 수 있다.
또한, Animation Tile, Rule Tile 등 유용한 타일 형태가 있다.
Animation Tile은 물이나 횃불 등 움직임이 들어가 있는 타일에 사용하면 좋다.
Rule Tile은 타일이 배치되었을 때, 미리 설정한 규칙에 의해 타일이 배치되도록 하는 역할이다.
InventoryUI
인벤토리의 크기는 상황마다 달라질 수 있다.
그럴 때마다 Texture를 다시 그릴 수는 없으니 9-Slice를 이용하여 해결할 수 있다.
Sprite Editor에서 위와 같이 늘어나도 되는 부분과 고정되어야 하는 부분을 분리한다.
이후, 이미지 타입을 Sliced로 변경하면 된다.
이제, ItemSlot을 추가해 보자.
우선, ItemSlot을 버튼으로 만들어 Prefab으로 만든다.
InventoryScript에서 이를 참조한다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Inventory : MonoBehaviour
{
[SerializeField] private GridLayoutGroup gridLayoutGroup;
private ItemButton[] buttons;
private ItemButton prevClickedButton;
void Awake()
{
buttons = gridLayoutGroup.GetComponentsInChildren<ItemButton>();
var itemManager = FindObjectOfType<ItemManager>();
foreach (var button in buttons)
{
var itemData = itemManager.itemDatas[Random.Range(0, itemManager.itemDatas.Count)];
button.GetComponent<Button>().onClick.AddListener(() =>
{
OnClickItemButton(button);
});
button.GetComponent<ItemButton>().ItemInfo = new ItemInfo()
{
amount = 1,
itemData = itemData
};
}
}
private void OnClickItemButton(ItemButton clickedButton)
{
if (prevClickedButton == null)
{
prevClickedButton = clickedButton;
return;
}
(prevClickedButton.ItemInfo, clickedButton.ItemInfo) = (clickedButton.ItemInfo, prevClickedButton.ItemInfo);
prevClickedButton = null;
}
}
Inventory가 Awake 되면 itemManager에서 ItemData를 받아와 임의의 값으로 Slot을 채운다.
각 Slot은 클릭되었을 때 OnClickItemButton함수가 실행되도록 Listener를 추가해 주었다.
OnClickItemButton에서는 현재 클릭된 버튼 이전에 클릭된 버튼이 있는지 확인하고 그러한 버튼이 있다면 현재 클릭된 버튼과 Swap 한다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class ItemButton : MonoBehaviour
{
private ItemInfo itemInfo;
[SerializeField] private Image itemImage;
public ItemInfo ItemInfo
{
get => itemInfo;
set
{
itemInfo = value;
SetItemImage(itemInfo.itemData.icon);
}
}
void SetItemImage(Sprite sprite)
{
itemImage.sprite = sprite;
itemImage.color = sprite == null ? Color.clear : Color.white;
}
}
버튼에서는 ItemInfo가 set 될 때마다 SetItemImage를 실행하도록 프로퍼티를 설정하였다.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "ItemData", menuName = "Datas/ItemData")]
public class ItemData : ScriptableObject
{
public string itemName;
public Sprite icon;
}
public class ItemInfo
{
public ItemData itemData;
public int amount;
}
public class ItemManager : MonoBehaviour
{
public List<ItemData> itemDatas = new List<ItemData>();
}
인벤토리를 관리할 Manager에서는 Item의 이름, 이미지에 대한 데이터를 저장하고 있다.