유니티 프로젝트/점토게임

[개발일지] 17. 가구 추가 & 애니메이션 적용

dubu0721 2025. 1. 19. 20:53

0. 들어가기 전에

이번엔 가구를 추가하고 점토를 가구에 드래그&드롭하면 애니메이션이 실행되도록 했다. 

 

가구는 그냥 게임 오브젝트 말고 캔버스 상에 Button 으로 추가했다. 마우스가 해당 게임 오브젝트로 진입했는지 여부를 파악하기 쉽게 하기 위함이다..

 

 


 

 

1. 게임 오브젝트

가구 UI 만 따로 관리하기 위해 새로운 캔버스 두 개를 만들었다. ToyCanvas1 에 있는 가구들은 점토보다 뒤에, ToyCanvas2 에 있는 가구는 점토보다 앞에 있도록 하기 위해 두개로 나눈 것이다.

 

 


 

 

2. 스크립트

이번에 새로 만든 스크립트는 ToyController 이고, 수정한 스크립트는 GameManager, Clay, ClayMove 이다.

 

2.1 ToyController 스크립트

using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI;
using UnityEngine;
using UnityEngine.EventSystems;

public class ToyController : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
{
    [Header("Toy Info")]
    public int toyIdx; // 장난감 인덱스(0: 캣타워, 1: 카펫, 2: 블라인더, 3: 조명)

    [Header("UI")]
    public Button toyButton;

    private void Awake()
    {
        toyButton = GetComponent<Button>(); // 버튼 할당받기
        SetButtonOnClick(); // onClick 컴포넌트 설정
    }

    private void SetButtonOnClick()
    {
        toyButton.onClick.RemoveAllListeners(); // 일단 다 지우기
        toyButton.onClick.AddListener(() => GameManager.instance.StartInfoPanel(GameManager.instance.toyInfo[toyIdx])); // 인덱스에 알맞는 가구 정보 가져와서 메서드 연결
    }

    public void SetInfo()
    {
        GameManager.instance.curToyIdx = toyIdx; // 현재 가구의 인덱스 넣어주깅
    }

    public void OnPointerEnter(PointerEventData eventData)
    {
        GameManager.instance.curToyIdx = toyIdx; // 현재 가구 인덱스 값으로 설정..
    }

    public void OnPointerExit(PointerEventData eventData)
    {
        GameManager.instance.curToyIdx = -1; // 아무 가구도 선택되지 않았음을 알리기..
    }
}

 

2.2 ToyController 스크립트 설명

더보기

1. 변수

장난감의 인덱스를 지정하기 위한 변수인 toyIdx 를 선언했다. 장난감은 Button 게임 오브젝트이기 때문에 Button 타입의 변수를 선언하여 관리할 수 있도록 했다.

[Header("Toy Info")]
public int toyIdx; // 장난감 인덱스(0: 캣타워, 1: 카펫, 2: 블라인더, 3: 조명)

[Header("UI")]
public Button toyButton;

 

 

2. Awake()

버튼 변수에 버튼 타입 컴포넌트를 할당해주고 버튼에 메서드를 연결하기 위해 SetButtonOnClick() 메서드를 호출했다. 

private void Awake()
{
    toyButton = GetComponent<Button>(); // 버튼 할당받기
    SetButtonOnClick(); // onClick 컴포넌트 설정
}

 

 

3. SetButtonOnClick()

일단 중복으로 연결 될 가능성을 없애기 위해 버튼에 연결된 메서드를 모두 지우고 시작했다.

 

장난감을 클릭하면 해당 장난감에 대한 설명을 띄우기 위해 StartInfoPanel 메서드를 연결했다.

private void SetButtonOnClick()
{
    toyButton.onClick.RemoveAllListeners(); // 일단 다 지우기
    toyButton.onClick.AddListener(() => GameManager.instance.StartInfoPanel(GameManager.instance.toyInfo[toyIdx])); // 인덱스에 알맞는 가구 정보 가져와서 메서드 연결
}

 

 

4. OnPointerEnter(PointerEventData eventData)

어떤 장난감의 애니메이션을 수행할 것인지 나타내기 위해 curToyIdx 의 값을 toyIdx 값으로 설정해주는 메서드이다.

 

버튼에 마우스가 진입하면 자동으로 호출된다.

public void OnPointerEnter(PointerEventData eventData)
{
    GameManager.instance.curToyIdx = toyIdx; // 현재 가구 인덱스 값으로 설정..
}

 

 

5. OnPointerExit(PointerEventData eventData)

마우스가 장난감을 벗어나면 다시 -1로 값을 변경해준다. 아무 장난감도 선택되지 않았음을 알리기 위함이다.

public void OnPointerExit(PointerEventData eventData)
{
    GameManager.instance.curToyIdx = -1; // 아무 가구도 선택되지 않았음을 알리기..
}

 

2.3 GameManager 스크립트

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

public class GameManager : MonoBehaviour
{
    [Header("Game Data")]
    public float love; // 애정
    public float gold; // 골드
    public bool[] unLockedClays; // 점토들의 해금 여부
    public bool[] catchedClays; // 야생에서 잡아왔는지 확인용
    public int clayHouseLevel = 1; // 점토 아파트 레벨
    public int clayClickLevel = 1; // 점토 클릭 레벨
    public int[] clayHouseLoveList; // 업그레이드 비용 
    public int[] clayClickLoveList; // 업그레이드 비용
    public int curPossibleClayNum = 1; // 최대로 키울 수 있는 점토의 개수(1레벨은 1마리, 2레벨은 2마리...)

    [Header("Game Manager")]
    public static GameManager instance; // 싱글톤 이용하기 위함

    [Header("GameDataUI")]
    public GameDataUIController gameDataUI;
    public delegate void SetInfoPanelHandler(string text); // 델리게이트 선언 
    public event SetInfoPanelHandler OnSetInfoPanel;

    [Header("Pool Manager")]
    public PoolManager poolManager;

    [Header("Coroutine")]
    public Coroutine updateTextUICoroutine;

    [Header("Game Exit")]
    public Button gameExitButton;

    [Header("Effect")]
    // 0: 점토 레벨업, 1: 점토 판매, 2: 점토 해금, 3: 업그레이드
    public ParticleSystem[] effectsPrefabs; // 프리팹 넣어놓기
    public ParticleSystem[] effects; // 관리용 변수
    public string[] effectGameObjectNames;

    [Header("Toy Control")]
    public int curToyIdx = -1; // 현재 선택된 장난감
    public RuntimeAnimatorController[] clayToyAnimators; // 가구랑 상호작용하는 애니메이터
    public string[] toyInfo; // 가구를 클릭하면 안내 판넬에 띄울 내용

    private void Awake()
    {
        // 싱글톤 패턴
        if (instance != null && instance != this)
        {
            // 이미 존재하면 새로 만든거 없애
            Destroy(gameObject);
            return;
        }

        instance = this;
        DontDestroyOnLoad(gameObject); // 얘는 다른 씬으로 전환되어도 안 없앨 거임

        poolManager = GameObject.Find("PoolManager").GetComponent<PoolManager>(); // 풀매니저 찾아서 할당

        for (int i=0; i < effectsPrefabs.Length; i++)
        {
            // 이펙트 생성해서 넣어놓기
            effects[i] = Instantiate(effectsPrefabs[i], GameObject.Find(effectGameObjectNames[i]).transform);
            effects[i].gameObject.SetActive(false); // 비활성화
        }
    }


    void Start()
    {
        gameExitButton = GameObject.Find("OptionPanelParent").transform.Find("Option Panel").transform.Find("Image").transform.Find("Exit Button").GetComponent<Button>();
        gameExitButton.onClick.AddListener(DataManager.instance.SaveGameData); // 게임 데이터 저장 메서드 연결
        gameExitButton.onClick.AddListener(GameExit); // 게임 종료 메서드 연결

        GetLove(0);
        GetGold(1000);

        // 저장된 게임 데이터가 있는 경우 데이터 가져와서 반영
        if (DataManager.instance.data.unlockClays != null)
        {
            // 저장된 데이터 반영해서 가져오기
            for (int i = 0; i < unLockedClays.Length; i++)
            {
                unLockedClays[i] = DataManager.instance.data.unlockClays[i];
                catchedClays[i] = DataManager.instance.data.catchClays[i];
            }
        }

        // 저장된 게임 수치 데이터가 있는 경우 데이터 가져와서 반영
        if (DataManager.instance.data.valueDatas != null)
        {
            // 저장된 데이터 반영해서 가져오기
            gold = DataManager.instance.data.valueDatas.gold;
            love = DataManager.instance.data.valueDatas.love;
            clayHouseLevel = DataManager.instance.data.valueDatas.clayHouseLevel;
            clayClickLevel = DataManager.instance.data.valueDatas.clayClickLevel;
            curPossibleClayNum = DataManager.instance.data.valueDatas.curPossibleClayNum;
        }

        // 메서드 연결하기
        DataManager.instance.OnSave -= SetSaveData; // 중복 방지하기 위해 먼저 빼줌
        DataManager.instance.OnSave += SetSaveData;
    }


    // 재화 얻는 함수
    public void GetGold(float capacity)
    {
        // 이미 코루틴이 종료되지 않은 중에 동일한게 또 들어오면 겹쳐서 반영이 돼서 이상하게 될 수 있으므로 null 인지 판단해야함. 
        if (updateTextUICoroutine != null)
        {
            StopCoroutine(updateTextUICoroutine);
        }

        updateTextUICoroutine = StartCoroutine(gameDataUI.UpdateTextUI("gold", gold + capacity, gold));
        gold += capacity;

        //PlayerPrefs.SetFloat("Gold", gold); // 데이터 저장
    }

    public void GetLove(float capacity)
    {
        // 이미 코루틴이 종료되지 않은 중에 동일한게 또 들어오면 겹쳐서 반영이 돼서 이상하게 될 수 있으므로 null 인지 판단해야함. 
        if (updateTextUICoroutine != null)
        {
            StopCoroutine(updateTextUICoroutine);
        }

        updateTextUICoroutine = StartCoroutine(gameDataUI.UpdateTextUI("love", love + capacity, love));
        love += capacity;

        //PlayerPrefs.SetFloat("Love", love); // 데이터 저장
    }

    public void SetSaveData()
    {
        int size = unLockedClays.Length;

        DataManager.instance.data.unlockClays = new List<bool>();
        DataManager.instance.data.catchClays = new List<bool>();

        for (int i = 0; i < size; i++)
        {
            DataManager.instance.data.unlockClays.Add(unLockedClays[i]); // 해금 여부 저장
            DataManager.instance.data.catchClays.Add(catchedClays[i]); // 포획 여부 저장
        }

        // 수치 데이터 저장
        DataManager.instance.data.valueDatas = new ValueDatas();
        DataManager.instance.data.valueDatas.gold = gold;
        DataManager.instance.data.valueDatas.love = love;
        DataManager.instance.data.valueDatas.clayHouseLevel = clayHouseLevel;
        DataManager.instance.data.valueDatas.clayClickLevel = clayClickLevel;
        DataManager.instance.data.valueDatas.curPossibleClayNum = curPossibleClayNum;
    }


    public void GameExit()
    {
        // 게임 종료
        Application.Quit();
    }

    
    public void StartInfoPanel(string text)
    {
        // 연결된 메서드 실행시키기
        OnSetInfoPanel?.Invoke(text);
    }
}

 

2.4 GameManager 스크립트 변경 사항 설명

더보기

1. 변수

장난감과 점토의 상호작용 애니메이션을 수행하기 위해 필요한 변수들을 선언했다. 각 변수의 의미는 주석과 같다.

[Header("Toy Control")]
public int curToyIdx = -1; // 현재 선택된 장난감
public Clay curSelectedClay; // 현재 선택된 점토
public RuntimeAnimatorController[] clayToyAnimators; // 가구랑 상호작용하는 애니메이터
public string[] toyInfo; // 가구를 클릭하면 안내 판넬에 띄울 내용

 

2.5 Clay 스크립트

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

public class Clay : MonoBehaviour
{
    [Header("Clay Data")]
    public float[] loves; // 점토를 클릭했을 때 얻는 애정 수치(1~5 레벨)
    public int[] touchCnts; // 해당 요소만큼 클릭되면 레벨업
    public int clayLevel = 1; // 1레벨에서 시작(5레벨까지 있음)
    public int curTouchCnt = 0;
    public int clayIdx; // 점토 소환할 때 필요한 인덱스
    public int buyPrice; // 구매 가격
    public int sellPrice; // 판매 가격
    public string clayName; // 점토 이름
    public int effectIdx = 0;

    [Header("Animation")]
    Animator anim; // 점토가 터치될 때 애니메이션 실행하기 위함
    public RuntimeAnimatorController[] animators;
    public float[] claySizes;

    [Header("Sprites")]
    public Sprite clay; // 점토 모습
    public Sprite animal; // 다 자란 동물 모습
    public GameObject shadow; // 이거 가구 애니메이션 중일 때는 끄도록..

    [Header("Drag Clay")]
    public float targetTime = 1; // 드래그 시작할 수 있는 시간
    public float curTime; // 현재 시간
    public Vector3 prevPos; // 점토를 드래그 하기 전 위치
    public bool isDragging; // 드래그 중인지 확인

    [Header("Pool Manager")]
    public PoolManager poolManager;

    [Header("ToyAnimation")]
    public bool isUsed; // 이미 사용되고 있는 중이면 클릭 버튼이 안 먹히도록 하는데 이용할 것..


    private void Awake()
    {
        anim = GetComponent<Animator>();
        shadow = transform.GetChild(0).gameObject;
    }

    private void Start()
    {
        poolManager = GameObject.Find("PoolManager").GetComponent<PoolManager>();
        SetClay(); // 점토 정보 반영 세팅(애니메이션, scale)
    }

    // 점토가 터치되면 저절로 호출됨
    private void OnMouseDown()
    {
        // toy 애니메이션이 수행되고 있는 중이면 빠져나가도록..
        if (isUsed) return;

        prevPos = transform.position; // 드래그 하기 전 위치 저장
        poolManager.curClay = gameObject.GetComponent<Clay>(); // 현재 선택된 클레이 지정

        Debug.Log("터치됨!");
        anim.SetTrigger("doTouch");
        GameManager.instance.GetLove(loves[clayLevel - 1] * GameManager.instance.clayClickLevel); // 레벨에 맞는 수치를 함수로 넘겨주기
        curTouchCnt++;
        LevelUpClay();
    }

    private void LevelUpClay()
    {
        // 이미 점토의 레벨이 최고 레벨에 도달했으면 밑으로 진입 안 하도록..
        if (clayLevel != 5)
        {
            if (curTouchCnt == touchCnts[clayLevel - 1])
            {
                clayLevel++; // 레벨 1 증가
                curTouchCnt = 0; // 초기화
                SetClay(); // 점토 정보 반영 세팅(애니메이션, scale)
                PlayEffect(effectIdx);

                if (clayLevel == 5)
                    gameObject.GetComponent<SpriteRenderer>().sprite = animal;
            }
        }
    }

    private void PlayEffect(int idx)
    {
        if (GameManager.instance.effects[idx].gameObject.activeSelf == false)
            GameManager.instance.effects[idx].gameObject.SetActive(true); // 활성화하기
        GameManager.instance.effects[idx].transform.position = transform.position; // 이펙트 위치를 점토 위치로 바꿔주기
        GameManager.instance.effects[idx].Play(); // 실행시키깅
    }

    // 점토 드래그할 때 호출되는 함수
    private void OnMouseDrag()
    {
        // toy 애니메이션이 수행되고 있는 중이면 빠져나가도록..
        if (isUsed) return;

        curTime += Time.deltaTime;
        isDragging = true;

        // 만약 현재 시간이 targetTime 보다 크거나 같다면 점토가 마우스를 따라오도록 하기..
        if (curTime >= targetTime)
        {
            // 마우스 위치를 가져온 후 Z 축을 카메라와의 거리로 설정한다
            Vector3 mousePosition = Input.mousePosition;
            mousePosition.z = Camera.main.WorldToScreenPoint(transform.position).z;

            // 스크린 좌표를 월드 좌표로 변환
            Vector3 worldPosition = Camera.main.ScreenToWorldPoint(mousePosition);

            // 오브젝트의 위치를 마우스의 월드 좌표로 이동
            transform.position = worldPosition;

            // 이 경우에는 UI 보다도 앞에 갈 수 있도록..
            gameObject.GetComponent<SpriteRenderer>().sortingOrder = 10;
        }
    }

    // 점토 내려놓을 때 호출되는 함수
    private void OnMouseUp()
    {
        // toy 애니메이션이 수행되고 있는 중이면 빠져나가도록..
        if (isUsed) return;

        curTime = 0;
        isDragging = false;

        if (transform.position.x < -6.5 || transform.position.x > 6.5 || transform.position.y < -3 || transform.position.y > 0.6)
            transform.position = prevPos;

        // 다시 UI 보다 아래로 가도록..
        gameObject.GetComponent<SpriteRenderer>().sortingOrder = -1;

        if (poolManager.isSellZone == true)
        {
            poolManager.isPossibleSell = true; // 판매 신호 주기
            poolManager.TrySellClay(); // 이벤트 호출
        }

        if (GameManager.instance.curToyIdx != -1)
            StartToyAnim(GameManager.instance.curToyIdx); // 가구 애니메이션 수행
    }

    public void ResetInfo(int level, int cnt)
    {
        if (level == 5)
            gameObject.GetComponent<SpriteRenderer>().sprite = animal; // 동물 모습으로 바꿔주기..

        // 점토 정보 설정
        clayLevel = level;
        curTouchCnt = cnt;

        SetClay();
    }

    public void ResetInfo()
    {
        // 점토 정보 초기화
        clayLevel = 1;
        curTouchCnt = 0;
        gameObject.GetComponent<SpriteRenderer>().sprite = clay; // 점토 모습으로 바꿔주기..

        SetClay();
    }

    private void SetClay()
    {
        anim.runtimeAnimatorController = animators[clayLevel - 1]; // 레벨에 맞는 애니메이터로 바꿔주기..
        gameObject.transform.localScale = new Vector3(claySizes[clayLevel - 1], claySizes[clayLevel - 1], 0); // 레벨에 맞게 사이즈 설정 

        // 끄고 다시 켜서 애니메이터에 반영
        anim.gameObject.SetActive(false);
        anim.gameObject.SetActive(true);
    }

    public void StartToyAnim(int idx)
    {
        shadow.SetActive(false); // 그림자 활성화 끄기

        Debug.Log("가구 애니메이션 시작했어여!!");
        isUsed = true; // 메서드 빠져나갈 때 false 로 다시 설정해주기..
        GetComponent<ClayMove>().isToyActioning = true;

        // 애니메이션 시작
        anim.runtimeAnimatorController = GameManager.instance.clayToyAnimators[idx];

        // 코루틴 시작
        StartCoroutine(PerformAnimation());
    }

    private IEnumerator PerformAnimation()
    {
        float duration = 3f;
        yield return new WaitForSeconds(duration);

        // 애니메이터 원상태로 돌려놓기
        SetClay();
        isUsed = false;
        GetComponent<ClayMove>().isToyActioning = false;
        transform.position = prevPos; // 이전 위치로 돌아가도록..
        shadow.SetActive(true); // 그림자 활성화 켜기

        Debug.Log("애니메이션 완료!");
    }
}

 

2.6 Clay 스크립트 변경 사항 설명

더보기

1. 변수

isUsed 변수를 선언해서 현재 점토가 장난감과 상호작용 하고 있는 중인지를 나타내도록 했다. 이 값을 통해 특정 로직의 수행을 잠시 멈추도록 할 수 있었다.

public GameObject shadow; // 이거 가구 애니메이션 중일 때는 끄도록..

[Header("ToyAnimation")]
public bool isUsed; // 이미 사용되고 있는 중이면 클릭 버튼이 안 먹히도록 하는데 이용할 것..

 

 

2. Awake()

장난감과 상호작용 할 때 점토의 자식 오브젝트인 그림자 오브젝트를 꺼야한다. 즉, 이를 조종하기 위해 shadow 변수에 게임 오브젝트를 할당했다.

shadow = transform.GetChild(0).gameObject;

 

 

3. Start()

다음과 같은 메서드를 호출하도록 했다. 점토의 애니메이션과 scale 을 설정하는 메서드이다.

SetClay(); // 점토 정보 반영 세팅(애니메이션, scale)

 

 

4. OnMouseDown(), OnMouseDrag(), OnMouseUp()

현재 점토가 장난감과 상호작용 하고 있는 중이라면 마우스로 점토를 클릭, 드래그 했을 때 기능이 수행되지 않도록 바로 빠져나오게 했다.

// toy 애니메이션이 수행되고 있는 중이면 빠져나가도록..
if (isUsed) return;

 

 

5. ResetInfo(int level, int cnt), ResetInfo()

두 메서드에 SetClay() 메서드를 호출하도록 했다. 기존의 레벨에 맞는 애니메이터로 변경하는 로직을 SetClay() 로 추출해서 만들었다. 

SetClay();

 

 

6. SetClay()

레벨에 맞는 애니메이터로 변경하는 부분과, 레벨에 맞게 점토의 scale 을 변경하는 부분이 존재한다.

 

그리고 변경된 점토의 scale 을 애니메이터에 반영하기 위해 애니메이터의 활성화를 껐다가 다시 켜도록 했다.

private void SetClay()
{
    anim.runtimeAnimatorController = animators[clayLevel - 1]; // 레벨에 맞는 애니메이터로 바꿔주기..
    gameObject.transform.localScale = new Vector3(claySizes[clayLevel - 1], claySizes[clayLevel - 1], 0); // 레벨에 맞게 사이즈 설정 

    // 끄고 다시 켜서 애니메이터에 반영
    anim.gameObject.SetActive(false);
    anim.gameObject.SetActive(true);
}

 

 

7. StartToyAnim(int idx)

장난감과 점토의 상호작용 애니메이션을 시작하도록 하는 메서드이다. 

 

그림자의 활성화를 끄고, isUsed 과 ClayMove 의 isToyActioning 값을 true 로 설정하여 상호작용이 시작되었음을 알렸다.

 

후에 매개변수로 받은 장난감의 인덱스로 해당하는 애니메이터를 가져와서 할당해주었다. 그 후 코루틴을 시작했다.

public void StartToyAnim(int idx)
{
    shadow.SetActive(false); // 그림자 활성화 끄기

    Debug.Log("가구 애니메이션 시작했어여!!");
    isUsed = true; // 메서드 빠져나갈 때 false 로 다시 설정해주기..
    GetComponent<ClayMove>().isToyActioning = true;

    // 애니메이션 시작
    anim.runtimeAnimatorController = GameManager.instance.clayToyAnimators[idx];

    // 코루틴 시작
    StartCoroutine(PerformAnimation());
}

 

 

8. PerformAnimation()

코루틴이다. 애니메이션이 수행되는 3초 동안 기다릴 수 있도록 만든 코루틴이다.

 

애니메이션의 수행이 종료되면 애니메이터의 상태를 원상태로 돌려놓고, isUsed 와 ClayMove 의 isToyActioning 값을 false 로 설정하여 상호작용이 종료되었음을 알렸다.

 

그리고 드래그 해서 가져오기 이전의 점토 위치로 돌아가도록 했다.

private IEnumerator PerformAnimation()
{
    float duration = 3f;
    yield return new WaitForSeconds(duration);

    // 애니메이터 원상태로 돌려놓기
    SetClay();
    isUsed = false;
    GetComponent<ClayMove>().isToyActioning = false;
    transform.position = prevPos; // 이전 위치로 돌아가도록..
    shadow.SetActive(true); // 그림자 활성화 켜기

    Debug.Log("애니메이션 완료!");
}

 

2.7 ClayMove 스크립트 변경 사항 설명

using JetBrains.Annotations;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


public enum MoveDirection
{
    Minus = -1, Plus = 1, Idle = 0 
}

public class ClayMove : MonoBehaviour
{
    [Header("ClayMovement")]
    public float moveSpeed; // 움직이는 속도

    public MoveDirection moveDirX; // x 축 방향
    public MoveDirection moveDirY; // y 축 방향

    public bool isMoving; // 돌아다니는지 여부 확인
    public bool isReturning; // 중점으로 돌아가는 중인지 여부 확인
    public bool isToyActioning; // 현재 가구 애니메이션이 수행되고 있는 중인지 확인

    public GameObject targetPosObj; // 중점에 있는 게임 오브젝트 할당해주기

    private Coroutine randomMoveCoroutine;

    [Header("Animation")]
    public Animator anim;


    void Start()
    {
        // 코루틴의 핸들을 저장하고 이를 통해 중단하는 것이 좋음
        randomMoveCoroutine = StartCoroutine(RandomMove());

        anim = GetComponent<Animator>();

        targetPosObj = GameObject.Find("TargetObj"); // 게임 오브젝트 찾아서 할당하기
    }

    private void OnEnable()
    {
        // 코루틴 중복 호출 막기
        if (randomMoveCoroutine != null)
        {
            StopCoroutine(randomMoveCoroutine);
        }
        randomMoveCoroutine = StartCoroutine(RandomMove());
        isReturning = false; // 활성화 시 반환 상태 초기화

        // 애니메이션도 초기황..
        if (anim != null)
        {
            anim.SetBool("isWalk", false);
        }
    }

    void Update()
    {
        // 점토가 움직일 수 있는 상황일 때만 아래 로직 수행하도록..
        if (!isReturning && !isToyActioning)
        {
            // Time.deltaTime 을 곱해주는 이유는 게임 오브젝트가 순간이동 하는 것을 막기 위함.
            transform.Translate(moveSpeed * Time.deltaTime * new Vector2((int)moveDirX, (int)moveDirY));

            // x 축 이동 방향이 오른쪽이면 sprite 를 뒤집도록..
            if (moveDirX == MoveDirection.Plus)
                gameObject.GetComponent<SpriteRenderer>().flipX = true;
            else if (moveDirX == MoveDirection.Minus)
                gameObject.GetComponent<SpriteRenderer>().flipX = false;
        }

        // 점토가 가구 애니메이션을 수행하지 않을 때 아래 로직 수행하도록..
        if (!isToyActioning)
        {
            // Idle 상태가 아니라면 walk 애니메이션 수행하도록
            if (moveDirX != 0 || moveDirY != 0)
            {
                anim.SetBool("isWalk", true);
            }
            else
            {
                anim.SetBool("isWalk", false);
            }
        }
    }

    // 점토가 비활성화 될 때..
    private void OnDisable()
    {
        // 코루틴 종료
        if (randomMoveCoroutine != null)
        {
            StopCoroutine(randomMoveCoroutine);
        }
    }

    // 3초에 한 번씩 이동 방향 바꾸도록 하는 코루틴..
    // 코루틴은 IEnumerator 를 반환함
    private IEnumerator RandomMove()
    {
        while (true)
        {
            // 방향 랜덤으로 설정
            moveDirX = (MoveDirection)Random.Range(-1, 2);
            moveDirY = (MoveDirection)Random.Range(-1, 2);

            yield return new WaitForSeconds(3f); // 3초 동안 기다령 
        }
    }

    private IEnumerator ReturnToCenter()
    {
        isReturning = true;

        float curTime = 0f;
        float duration = 5f; // 중점으로 돌아가는 시간 5초 만큼 줄 것..

        Vector2 targetPos = targetPosObj.transform.position; // 중점에 있는 오브젝트의 위치 할당

        while (curTime < duration)
        {
            // 현재 점토의 위치가 목표 지점보다 왼쪽에 있으면 오른쪽을 보도록..
            if (targetPos.x > transform.position.x)
                gameObject.GetComponent<SpriteRenderer>().flipX = true;
            else if (targetPos.x < transform.position.x)
                gameObject.GetComponent<SpriteRenderer>().flipX = false;

            transform.position = Vector2.MoveTowards(transform.position, targetPos, moveSpeed * Time.deltaTime * 1.5f);
            curTime += Time.deltaTime;

            yield return null; // 다음 프레임까지 대기
        }

        isReturning = false; // 랜덤 이동 재개
        randomMoveCoroutine = StartCoroutine(RandomMove()); // 다시 시작시키기
    }


    // 콜라이더 컴포넌트를 가진 게임 오브젝트랑 부딪히면 이 함수로 진입
    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (isToyActioning) return; // 만약 지금 토이 액션 중이면 그냥 나가도록..

        if (isReturning) return; // 만약 이미 돌아가고 있는 중에 또 부딪히면 걍 나가라..

        // 부딪힌 게임 오브젝트의 태그가 Target 이면 ReturnToCenter 코루틴 호출하도록
        if (collision.gameObject.CompareTag("Wall"))
        {
            // 랜덤 이동 코루틴은 중단시키기
            StopCoroutine(randomMoveCoroutine);

            // 중앙 복귀 코루틴 호출
            StartCoroutine(ReturnToCenter());
        }
    }
}

 

2.8 ClayMove 스크립트 변경 사항 설명

더보기

1. Update()

기존 if 문에 !isToyActioning 조건을 추가해줬다. 즉, 점토가 움직일 수 있는 상황일 때만 로직을 수행할 수 있도록 했다. isToyActioning 의 값이 true 라는 것은 점토가 장난감과 상호작용 하는 중이므로 움직일 수 없는 상태임을 의미한다.

 

그리고 점토가 가구 애니메이션을 수행하는 중에는 기존 일반 애니메이터를 사용하지 않으므로 SetBool 을 이용하는 로직을 수행하지 않도록 하기 위해 if 문을 추가로 더해줬다.

void Update()
{
    // 점토가 움직일 수 있는 상황일 때만 아래 로직 수행하도록..
    if (!isReturning && !isToyActioning)
    {
        // Time.deltaTime 을 곱해주는 이유는 게임 오브젝트가 순간이동 하는 것을 막기 위함.
        transform.Translate(moveSpeed * Time.deltaTime * new Vector2((int)moveDirX, (int)moveDirY));

        // x 축 이동 방향이 오른쪽이면 sprite 를 뒤집도록..
        if (moveDirX == MoveDirection.Plus)
            gameObject.GetComponent<SpriteRenderer>().flipX = true;
        else if (moveDirX == MoveDirection.Minus)
            gameObject.GetComponent<SpriteRenderer>().flipX = false;
    }

    // 점토가 가구 애니메이션을 수행하지 않을 때 아래 로직 수행하도록..
    if (!isToyActioning)
    {
        // Idle 상태가 아니라면 walk 애니메이션 수행하도록
        if (moveDirX != 0 || moveDirY != 0)
        {
            anim.SetBool("isWalk", true);
        }
        else
        {
            anim.SetBool("isWalk", false);
        }
    }
}

 

 

2. OnCollisionEnter2D(Collision2D collision)

점토가 벽에 닿으면 중앙으로 복귀하도록 하는 메서드이다. 근데 점토가 장난감과 상호작용 중이라면 중간으로 돌아가는 로직이 수행되면 안되기 때문에 맨 처음에 진입하자마자 isToyActioning 의 값을 확인하도록 했다. 만약 점토가 애니메이션 중이라면 바로 빠져나가도록 했다.

// 콜라이더 컴포넌트를 가진 게임 오브젝트랑 부딪히면 이 함수로 진입
private void OnCollisionEnter2D(Collision2D collision)
{
    if (isToyActioning) return; // 만약 지금 토이 액션 중이면 그냥 나가도록..

    if (isReturning) return; // 만약 이미 돌아가고 있는 중에 또 부딪히면 걍 나가라..

    // 부딪힌 게임 오브젝트의 태그가 Target 이면 ReturnToCenter 코루틴 호출하도록
    if (collision.gameObject.CompareTag("Wall"))
    {
        // 랜덤 이동 코루틴은 중단시키기
        StopCoroutine(randomMoveCoroutine);

        // 중앙 복귀 코루틴 호출
        StartCoroutine(ReturnToCenter());
    }
}

OnCollisionEnter2D(Collision2D collision)

 

 


 

 

3. 결과물