Animation Curve
EaseInOut 같은 커브를 지원하는 클래스이다.
public AnimationCurve curve = AnimationCurve.Linear(0,0,1,1);
Vector3 newPosition = Vector3.Lerp(itemBeginPOS, boxTransform.position, curve.Evaluate(t / duration));
Item Spawner
아이템을 동적으로 생성하는 spawner를 만들어 보자.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ItemSpawner : MonoBehaviour
{
public GameObject itemPrefab;
public float minSpawnTime;
public float maxSpawnTime;
void Start()
{
StartCoroutine(SpawnItem());
}
IEnumerator SpawnItem()
{
GameObject item = Instantiate(itemPrefab, transform.position, Quaternion.identity);
float interval = Random.Range(minSpawnTime, maxSpawnTime);
yield return new WaitForSeconds(interval);
StartCoroutine(SpawnItem());
}
}
우선, 임의의 시간 이후 아이템이 생성되도록 만들었다.
이러면 아이템이 계속해서 생성된다.
아이템이 획득해서 사라지면 다음 아이템을 생성하도록 만들어 보자.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ItemSpawner : MonoBehaviour
{
public GameObject itemPrefab;
public float minSpawnTime;
public float maxSpawnTime;
void Start()
{
SpawnItem();
}
IEnumerator SpawnItemForLoop()
{
float interval = Random.Range(minSpawnTime, maxSpawnTime);
yield return new WaitForSeconds(interval);
SpawnItem();
}
private void SpawnItem()
{
GameObject item = Instantiate(itemPrefab, transform.position, Quaternion.identity);
item.GetComponent<SpawnedItem>().OnDestroied += () =>
{
StartCoroutine(SpawnItemForLoop());
};
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SpawnedItem : MonoBehaviour
{
public Action OnDestroied;
private void OnDestroy()
{
OnDestroied?.Invoke();
}
}
Action이란 일종의 델리게이트이다.
지금의 경우에는 오브젝트가 소멸될 때 Invoke 한다.
ItemSpawner에서는 아이템을 생성하여 OnDestried라는 Action에 람다함수를 바인딩한다.
해당 Action이 Invoke 되면 람다함수가 실행되어 다른 아이템을 생성하게 된다.
Inventory 연동
아이템을 획득하면 인벤토리에 저장되고 UI에 표시되도록 만들어 보자.
우선, Inventory를 나타낼 UI를 Canvas에 추가하자.
해당 UI는 다음과 같은 스크립트를 갖고 있다.
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>();
foreach (var button in buttons)
{
button.GetComponent<Button>().onClick.AddListener(() =>
{
OnClickItemButton(button);
});
}
}
private void OnClickItemButton(ItemButton clickedButton)
{
if (prevClickedButton == null)
{
prevClickedButton = clickedButton;
return;
}
(prevClickedButton.ItemInfo, clickedButton.ItemInfo) = (clickedButton.ItemInfo, prevClickedButton.ItemInfo);
prevClickedButton = null;
}
public void AddItem(SpawnedItem item)
{
foreach (var button in buttons)
{
if (button.ItemInfo == null)
{
button.ItemInfo = new ItemInfo(){itemData = item.itemData, amount = 1};
break;
}
}
}
}
AddItem을 통해 인벤토리에 item을 추가한다.
이제 캐릭터가 아이템과 충돌하면 Inventory에 아이템을 추가하도록 하면 된다.
이는 ItemGetter에서 실행하도록 하자.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class ItemGetter : MonoBehaviour
{
public RectTransform itemRectTransform;
public Canvas canvas;
public GettedObject itemPrefab;
public Inventory inventory;
public Camera uiCamera;
private void OnTriggerEnter2D(Collider2D other)
{
if (Camera.main == null) return;
var newObject = Instantiate(itemPrefab, other.transform.position, Quaternion.identity, canvas.transform);
newObject.SetItemData(other.GetComponent<SpawnedItem>().itemData);
newObject.transform.position = other.transform.position;
newObject.OnReached += () =>
{
inventory.AddItem(newObject.itemData);
};
var screenPos = Camera.main.WorldToScreenPoint(newObject.transform.position);
Vector2 localPoint;
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvas.GetComponent<RectTransform>(), screenPos, uiCamera, out localPoint);
newObject.transform.localPosition = localPoint;
newObject.TargetPos = itemRectTransform;
Destroy(other.gameObject);
}
}
새로 생성한 UI가 인벤토리 UI에 도달했다면 Item을 추가하도록 Action을 통한 델리게이트를 이용했다.
그리고 아이템과 아이템을 획득했을 때 상자로 이동하는 UI의 sprite를 설정해 주어야 하기 때문에 다음과 같이 ItemData를 추가한다.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SpawnedItem : MonoBehaviour
{
public Action OnDestroied;
public ItemData itemData;
private void OnDestroy()
{
OnDestroied?.Invoke();
}
public void SetItemData(ItemData data)
{
GetComponent<SpriteRenderer>().sprite = data.icon;
itemData = data;
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class GettedObject : MonoBehaviour
{
public GameObject reachFX;
public AnimationCurve curve = AnimationCurve.Linear(0,0,1,1);
public ItemData itemData;
public Action OnReached;
public RectTransform TargetPos
{
set => StartCoroutine(MoveToTarget(value));
}
private float EaseInOut(float t)
{
return t < 0.5f ? 2f * t * t : -1f + (4f - 2f * t) * t;
}
private IEnumerator MoveToTarget(RectTransform boxTransform)
{
float duration = 1.0f;
float t = 0.0f;
Vector3 itemBeginPOS = transform.position;
while (t <= duration)
{
Vector3 newPosition = Vector3.Lerp(itemBeginPOS, boxTransform.position, curve.Evaluate(t / duration));
transform.position = newPosition;
t += Time.deltaTime;
yield return null;
}
var particle = Instantiate(reachFX, boxTransform.position, reachFX.transform.rotation);
var vector3 = particle.transform.position;
vector3.z = 1f;
particle.transform.position = vector3;
Destroy(particle, 3.0f);
var inventoryButton = boxTransform.gameObject.GetComponent<InventoryButton>();
inventoryButton.Shake();
OnReached?.Invoke();
transform.position = boxTransform.position;
Destroy(gameObject);
}
public void SetItemData(ItemData data)
{
GetComponent<Image>().sprite = data.icon;
itemData = data;
}
}
이제, 아이템 매니저를 통해 여러 개의 아이템을 생성할 수 있도록 해보자.
ItemSpawner에서 아이템 데이터를 관리하는 아이템 매니저를 참조하여 데이터를 랜덤으로 생성하고 이를 통해 아이템을 생성하도록 변경해 보자.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Random = UnityEngine.Random;
public class ItemSpawner : MonoBehaviour
{
public GameObject itemPrefab;
public float minSpawnTime;
public float maxSpawnTime;
public ItemManager _ItemManager;
void Start()
{
SpawnItem();
}
IEnumerator SpawnItemForLoop()
{
float interval = Random.Range(minSpawnTime, maxSpawnTime);
yield return new WaitForSeconds(interval);
SpawnItem();
}
private void SpawnItem()
{
GameObject item = Instantiate(itemPrefab, transform.position, Quaternion.identity);
var spawnedItem = item.GetComponent<SpawnedItem>();
int rand = Random.Range(0, _ItemManager.itemDatas.Count);
spawnedItem.SetItemData(_ItemManager.itemDatas[rand]);
spawnedItem.OnDestroied += () =>
{
StartCoroutine(SpawnItemForLoop());
};
}
}
이제 아이템은 매니저에 설정된 아이템 중 랜덤 하게 생성될 것이다.
이를 획득했을 때, 인벤토리에 같은 아이템이 있다면 수량을 증가시키고 새로운 아이템이라면 다른 슬롯에 추가하도록 만들어 보자.
우선, 아이템의 수량을 확인할 Text를 아이템 버튼에 추가하자.
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class ItemButton : MonoBehaviour
{
private ItemInfo itemInfo;
[SerializeField] private Image itemImage;
[SerializeField] private TMP_Text itemAmountTxt;
public ItemInfo ItemInfo
{
get => itemInfo;
set
{
itemInfo = value;
SetItemImage(itemInfo.itemData.icon);
SetItemText(itemInfo.amount);
}
}
private void SetItemText(int amount)
{
itemAmountTxt.SetText(amount == 0 ? "" : $"{amount}");
}
void SetItemImage(Sprite sprite)
{
itemImage.sprite = sprite;
itemImage.color = sprite == null ? Color.clear : Color.white;
}
}
이제 인벤토리에서 아이템을 획득하는 AddItem함수에서 동일한 종류의 아이템이 있는지 확인하는 부분을 추가하면 된다.
public void AddItem(SpawnedItem item)
{
//기존 아이템 확인
foreach (var button in buttons)
{
if(button.ItemInfo == null) continue;
if (button.ItemInfo.itemData == item.itemData)
{
button.ItemInfo = new ItemInfo(){ itemData = button.ItemInfo.itemData, amount = button.ItemInfo.amount + 1 };
return;
}
}
//추가
foreach (var button in buttons)
{
if (button.ItemInfo == null)
{
button.ItemInfo = new ItemInfo(){itemData = item.itemData, amount = 1};
break;
}
}
}
Monster
몬스터를 생성하고 게임에 적용해 보자.
우선, 좌우로 움직이는 몬스터를 만들어 보자.
몬스터는 다음과 같은 스크립트를 갖는다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Monster : MonoBehaviour
{
private SpriteRenderer _spriteRenderer;
private Animator _animator;
private Rigidbody2D _rigidbody;
public float Speed = 5f;
public int switchCount = 0;
public int moveCount = 0;
public Vector2 direction;
void Start()
{
_spriteRenderer = GetComponent<SpriteRenderer>();
_animator = GetComponent<Animator>();
_rigidbody = GetComponent<Rigidbody2D>();
}
void FixedUpdate()
{
if (direction.x != 0) _spriteRenderer.flipX = direction.x < 0;
transform.position += new Vector3(direction.x * Speed * Time.deltaTime, 0, 0);
if (moveCount++ >= switchCount)
{
direction *= -1;
_spriteRenderer.flipX = direction.x < 0;
moveCount = 0;
}
}
}
정해진 프레임동안 현재 방향으로 이동하며 일정 기간이 지나면 반대 방향으로 이동한다.
이제 몬스터와 플레이어의 충돌이 가능하도록 변경해 보자.
현재, 몬스터와 플레이어 모두 Collider와 Rigidbody를 갖고 있지만 해당 컴포넌트는 움직임을 구현하기 위해 사용된다.
이를 충돌도 가능하도록 할 수는 있지만, 따로 분리하는 것이 다양한 상황을 컨트롤할 수 있어 좋다.
그리고 각 Collision역할을 할 컴포넌트에 Layer를 설정해 다른 오브젝트와의 충돌을 감지하지 않도록 설정하자.
몬스터는 MonsterHitCollision, 플레이어는 PlayerHitCollision으로 설정하였다.
MonsterHitCollision의 Exclude에서 PlayerHitCollision만 제외시킨 점을 유의하자.
이렇게 설정하면 PlayerHitCollision과의 충돌만 판정한다.
또한, IsTrigger가 걸려있기 때문에 OnTirggerEnter2D로 Callback이 온다.
그리고 Rigidbody는 부모에 있기 때문에 HitCollision에는 없어도 부모와 함께 충돌 판정이 일어난다.
따라서, 몬스터에서 OnTirggerEnter2D를 구현하면 된다.
private void OnTriggerEnter2D(Collider2D other)
{
direction *= -1;
moveCount = 0;
Vector3 backPos = other.transform.position - transform.position;
backPos += Vector3.up;
backPos.Normalize();
other.attachedRigidbody.AddForceAtPosition(backPos * KnockBack, transform.position, ForceMode2D.Impulse);
}
이 함수에서 other은 무조건 플레이어이다.(HitCollision)
몬스터에서 플레이어의 방향으로 튕겨 나가게 만들며 위로 살짝 뛰어오르게 만들기 위해 Vector3.up벡터를 더해주었다.
AddForceAtPosition을 통해 특정 위치에서 충격을 주어 플레이어의 velocity에 영향을 주면 된다.
만약, 이 코드로 플레이어가 넉백 되지 않는다면 플레이어의 이동 코드를 살펴봐야 한다.
현재, 플레이어의 이동 코드에서는 InputSystem에서 전달하는 moveValue를 받아와 velocity를 설정한다.
이렇게 진행하게 되면 플레이어의 velocity는 무조건 입력에만 영향을 받게 된다.
즉, 현재 velocity를 유지해 주는 부분이 사라진다.
이를 다음과 같이 수정하면 된다.
void Update()
{
Vector2 moveValue = Move_Input.ReadValue<Vector2>();
if (moveValue.x != 0)
{
_spriteRenderer.flipX = moveValue.x < 0;
_rigidbody.velocity = Vector3.up * _rigidbody.velocity.y;
}
_animator.SetFloat(SpeedId, Mathf.Abs(moveValue.x));
transform.position += new Vector3(moveValue.x * Speed * Time.deltaTime, 0);
if (Jump_Input.triggered && IsGround)
{
_rigidbody.AddForce(Vector2.up * JumpSpeed, ForceMode2D.Impulse);
_animator.Play("Alchemist_Jump");
}
}
moveValue.x가 0이 아니라면 입력이 있다는 뜻이니 현재 velocity의 x값을 0으로 만들어 입력을 곧바로 수행할 수 있도록 해준다.
그리고 velocity를 더하는 방식으로 이동하지 않고 position을 변경해 주는 방식으로 진행한다면 velocity에 의한 영향을 계속해서 유지할 수 있다.
추가로 physicMaterial의 friction이 0이라면 한 번 받은 힘이 절대 줄지 않아 끝까지 이동한다.
따라서, friction을 기존값으로 변경해야 한다.