0. 들어가기 전에
이번엔 야생 UI 를 손봤다. 체력바에 현재 체력을 표시하도록 했고, 화면 상단에 타이머를 야생이 끝나기 까지 남은 시간으로 설정하도록 했다.
그리고 야생의 난이도가 어려워질수록 배경이 어두워지는데 타일맵의 색은 그대로라 위화감이 있어서 Light2D 를 이용했다. 일단 플레이어한테 빛을 넣어놨고, 맵에 global light 을 만들어놨다. 맵이 어려워질수록 global light 을 어둡게 해서 타일맵의 색도 어두워지도록 했다.
그리고 야생에서 새로운 점토를 잡아왔을 때 메인 씬에서 점토 구매 버튼 위에 별표가 뜨도록 했다. 새로 잡은 점토 페이지로 도달하기 전까지는 별표가 없어지지 않는다.
1. 게임 오브젝트
메인 씬과 야생 씬으로 파트를 나누어서 설명하려고 한다.
1.1 메인 씬
1.1.1 New Alam
야생에서 새로운 점토를 잡아오면 사진과 같이 별표가 뜨도록 했다. 잡아온 점토의 페이지로 도달할 때까지 별표가 활성화되어 있도록 했다.
1.2 야생 씬
야생 씬에는 빛 오브젝트를 추가했고, 게임 클리어 판넬에 NewText 게임 오브젝트를 추가했다. 그리고 BGM 게임 오브젝트도 만들었다.
1.2.1 NewText
이번에 잡은 점토가 처음 만나는 점토라면 알리기 위해서 NewText 를 만들었다. 애니메이션도 만들어서 위아래로 움직이도록 했다.
1.2.2 Light 2D, light
Light2D 는 게임 화면을 어둡게 만들기 위해서 만들었고, 플레이어 속 light 는 플레이어 주변을 밝히기 위해 만들었다.
예시 모습은 다음과 같다.
1.2.3 BGM
BGM 게임 오브젝트를 추가해서 배경음을 야생 난이도에 맞게 설정할 수 있도록 했다.
2. 스크립트
이번에 새로 만든 스크립트는 BGM 이다.
2.1 BGM 스크립트
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BGM : MonoBehaviour
{
[Header("Audio")]
private AudioSource bgmAudio;
public AudioClip easyClip;
public AudioClip normalClip;
public AudioClip hardClip;
private void Awake()
{
bgmAudio = GetComponent<AudioSource>();
}
private void Start()
{
SetBGM();
}
private void SetBGM()
{
// 야생 난이도에 맞는 브금 틀도록
if (GameManager.instance.worldLevel == 0)
bgmAudio.clip = easyClip;
else if (GameManager.instance.worldLevel == 1)
bgmAudio.clip = normalClip;
else
bgmAudio.clip = hardClip;
bgmAudio.Play(); // 재생!
}
public void StopBGM()
{
bgmAudio?.Stop();
}
}
2.2 BGM 스크립트 설명
1. 변수
야생 단계에 맞게 브금을 설정하기 위한 변수이다.
[Header("Audio")]
private AudioSource bgmAudio;
public AudioClip easyClip;
public AudioClip normalClip;
public AudioClip hardClip;
2. Awake()
할당해줬다.
private void Awake()
{
bgmAudio = GetComponent<AudioSource>();
}
3. Start()
브금을 틀도록 했다.
private void Start()
{
SetBGM();
}
4. SetBGM()
야생 난이도에 맞게 브금을 설정하고 실행하도록 했다.
private void SetBGM()
{
// 야생 난이도에 맞는 브금 틀도록
if (GameManager.instance.worldLevel == 0)
bgmAudio.clip = easyClip;
else if (GameManager.instance.worldLevel == 1)
bgmAudio.clip = normalClip;
else
bgmAudio.clip = hardClip;
bgmAudio.Play(); // 재생!
}
5. StopBGM()
브금을 끄는 메서드이다. 다른 스크립트에서 호출해서 사용한다.
public void StopBGM()
{
bgmAudio?.Stop();
}
2.3 Player 스크립트
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Runtime.InteropServices;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.Rendering.Universal;
using UnityEngine.UI;
public class Player : MonoBehaviour
{
[Header("Player Info")]
public int heart = 3; // 목숨 3개
public float jumpPower = 500f; // 점프 힘
public int jumpCount = 0; // 누적 점프 횟수(최대 두번 뛸 수 있도록)
public bool isGrounded = false; // 바닥에 닿았는지 여부
public bool isDead = false; // 사망 여부
public bool isInvincibility = false; // 무적 여부
public Rigidbody2D rigid; // 리지드바디 컴포넌트
public Animator anim; // 애니메이터 컴포넌트
public AudioSource playerAudio; // 사용할 오디오 소스 컴포넌트
public SpriteRenderer spriteRenderer; // 스프라이트 렌더러
public AudioClip deathClip; // 죽을 때 나는 소리
public AudioClip jumpClip; // 점프할 때 나는 소리
public AudioClip attackedClip; // 맞을 때 나는 소리
public AudioClip gameClearClip; // 게임 성공할 때 나는 소리
public float gamePlayTime = 30f; // 일단 1분으로..
public float curTime = 0;
[Header("World UI")]
public BGM worldBGM;
public Text time;
public GameObject[] heartUIs;
public Vector3[] heartUIsPos; // 애니메이션 수행하기 전 위치
public int heartIdx;
[Header("Light")]
public Light2D playerLight;
private void Start()
{
rigid = GetComponent<Rigidbody2D>();
anim = GetComponent<Animator>();
playerAudio = GetComponent<AudioSource>();
spriteRenderer = GetComponent<SpriteRenderer>();
for (int i=0; i<heartUIs.Length; i++)
{
// 위치 저장
heartUIsPos[i] = heartUIs[i].transform.position;
}
time.gameObject.SetActive(false); // 게임 시작 전에 타이머 꺼놓기
// 맵 레벨에 따라 설정
if (GameManager.instance.worldLevel == 0)
playerLight.gameObject.SetActive(false);
if (GameManager.instance.worldLevel == 2)
time.color = new Color(255, 255, 255, 255); // 텍스트 흰색으로 바꾸기..
}
private void Update()
{
// 아직 게임 시작 안 했으면 좀 기다리기..
while (!GameManager.instance.isGameStart)
{
return;
}
time.gameObject.SetActive(true); // 게임 시작하면 활성화 켜기
if (curTime >= gamePlayTime)
{
// 플레이 시간 지나면 더이상 진행하지 않음
GameEnd();
}
if (isDead)
{
// 사망하면 더이상 진행하지 않음
return;
}
if (Input.GetMouseButtonDown(0) && jumpCount < 2)
{
// 점프 횟수 증가
jumpCount++;
// 점프 직전에 속도를 0 으로 변경
rigid.velocity = Vector3.zero;
// 위쪽으로 힘주기
rigid.AddForce(new Vector2(0, jumpPower));
// 오디오 소스 재생
PlaySound(jumpClip);
}
else if (Input.GetMouseButtonUp(0) && rigid.velocity.y > 0)
{
// 마우스 왼쪽 버튼에서 손을 떼는 순간 && 속도의 y 값이 양수(위로 상승 중)
// 현재 속도를 절반으로 변경
rigid.velocity = rigid.velocity * 0.5f;
}
// 애니메이터의 Grounded 파라미터를 isGrounded 값으로 갱신
anim.SetBool("Grounded", isGrounded);
curTime += Time.deltaTime; // 시간 더해주기
// Time Text 에 시간 반영
time.text = (int)(gamePlayTime - curTime + 1) + "s";
}
private void PlaySound(AudioClip clip)
{
playerAudio.clip = clip;
playerAudio.Play();
}
public void GameEnd()
{
worldBGM.StopBGM(); // 스탑!
// 오디오 소스 재생
PlaySound(gameClearClip);
rigid.simulated = false;
// 여기서 게임 매니저 게임 종료(성공) 메서드 호출
GameManager.instance.WorldGameClear();
}
public void Die()
{
worldBGM.StopBGM(); // 스탑!
// 오디오 소스 재생
PlaySound(deathClip);
for (int i=heartIdx; i<heartUIs.Length; i++)
{
SetHeartUI(i); // 설정해용~~
}
StartCoroutine(DieCoroutine());
}
private void OnTriggerEnter2D(Collider2D collision)
{
// Collider 를 통해 다른 객체가 경계를 통과했을 때 자동으로 호출되는 메서드
// 두 객체 중 적어도 하나에 Collider 가 있고, Is Trigger 옵션이 활성화 되어 있어야함.
// 체력 깎이는 부분은 무적 상태라면 수행 안 하고 걍 빠져나가기..
if (isInvincibility) return;
// 만약 플레이어가 체력 깎이는 바닥을 통과했을 때
if (collision.tag == "AttackGround")
{
Die(); // 죽어
}
}
private void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.tag == "Ground")
{
// 충돌한 가장 낮은 지점 확인
ContactPoint2D contact = collision.contacts[0];
// 플레이어의 발보다 높은 곳에서 충돌했다면 무시
if (contact.point.y > transform.position.y) return;
isGrounded = true; // 땅에 닿았음 표시
jumpCount = 0; // 리셋
}
else if (collision.gameObject.tag == "Attack")
{
if (isInvincibility) return; // 무적 상태면 그냥 나가기..
// 장애물에 닿았으면 heart 값 -1 해주기..
Attacked();
}
else if (collision.gameObject.tag == "Monster")
{
if (isInvincibility) return; // 무적 상태면 그냥 나가기..
// 충돌한 가장 낮은 지점 확인
ContactPoint2D contact = collision.contacts[0];
// 만약 플레이어가 몬스터보다 위에 있고, y 축의 속도가 감소하는 중이라면 몬스터를 밟은 거임
if (transform.position.y >= contact.point.y /*&& rigid.velocity.y <= 0*/)
{
// 몬스터 머리 밟으면 다시 이단 점프 가능하도록..
isGrounded = true; // 땅에 닿았음 표시
jumpCount = 0; // 리셋
// 몬스터의 Die 메서드 호출
collision.transform.GetComponent<Monster>().Die();
}
else
{
Attacked();
}
}
}
private void OnCollisionExit2D(Collision2D collision)
{
if (collision.gameObject.tag == "Ground")
{
isGrounded = false;
}
}
private void Attacked()
{
// 오디오 소스 재생
PlaySound(attackedClip);
// 피 깎기
heart--;
SetHeartUI(heartIdx);
heartIdx++;
if (heart == 0)
{
Die(); // 죽어
return;
}
// 잠시 무적 상태로..
StartCoroutine(Invincibility());
}
private void SetHeartUI(int idx)
{
heartUIs[idx].GetComponent<Animator>().Play("Idle", 0, 0f); // 특정 애니메이션 초기화
heartUIs[idx].GetComponent<Animator>().enabled = false;
heartUIs[idx].transform.position = heartUIsPos[idx]; // 위치 바꿩
heartUIs[idx].GetComponent<Image>().color = new Color32(255, 255, 255, 40);
}
private IEnumerator Invincibility()
{
isInvincibility = true;
spriteRenderer.color = new Color32(255, 0, 0, 180); // 반투명하게..(빨갛게)
gameObject.layer = 9; // PlayerDamaged 레이어는 9번임
anim.SetTrigger("Hit"); // 애니메이션 수행
yield return new WaitForSeconds(2f); // 2초 동안 무적
gameObject.layer = 8; // Player 레이어는 8번임
spriteRenderer.color = new Color32(255, 255, 255, 255); // 원래 상태로..
isInvincibility = false; // 다시 false 로 바꾸고 빠져나가기..
}
private IEnumerator DieCoroutine()
{
// 여기서 게임 종료(실패) 로직 수행할거임
GameManager.instance.WorldGameFail(); // 게임 실패 정보 세팅
// 애니메이터의 Die 트리거 파라미터를 세팅함
anim.SetTrigger("Die");
// 오디오 소스 클릅을 deathClip 으로 변경
playerAudio.clip = deathClip;
// 오디오 실행
playerAudio.Play();
// 속도를 제로로 변경
rigid.velocity = Vector2.zero;
// 사망 상태 true 로
isDead = true;
yield return new WaitForSeconds(3f);
gameObject.SetActive(false); // 비활성화
}
}
2.4 Player 스크립트 변경 사항 설명
1. 변수
야생의 체력바 UI 를 관리할 수 있도록 변수를 선언했다. 그리고 플레이어의 자식 오브젝트인 light 를 관리하기 위한 변수도 선언했다.
[Header("World UI")]
public BGM worldBGM;
public Text time;
public GameObject[] heartUIs;
public Vector3[] heartUIsPos; // 애니메이션 수행하기 전 위치
public int heartIdx;
[Header("Light")]
public Light2D playerLight;
2. Start()
애니메이션을 수행하기 전의 체력바 위치를 저장하도록 했다. 체력이 까이면 해당 체력바가 더이상 애니메이션을 수행하지 않도록 해야한다. 그때 위치가 잘못 설정될 수 있어서 애니메이션을 멈춘 후 따로 위치를 다시 설정해주기 위해 필요하다.
그리고 게임 시작 전엔 time 게임 오브젝트를 꺼서 보이지 않도록 했다.
private void Start()
{
rigid = GetComponent<Rigidbody2D>();
anim = GetComponent<Animator>();
playerAudio = GetComponent<AudioSource>();
spriteRenderer = GetComponent<SpriteRenderer>();
for (int i=0; i<heartUIs.Length; i++)
{
// 위치 저장
heartUIsPos[i] = heartUIs[i].transform.position;
}
time.gameObject.SetActive(false); // 게임 시작 전에 타이머 꺼놓기
// 맵 레벨에 따라 설정
if (GameManager.instance.worldLevel == 0)
playerLight.gameObject.SetActive(false);
if (GameManager.instance.worldLevel == 2)
time.color = new Color(255, 255, 255, 255); // 텍스트 흰색으로 바꾸기..
}
3. Update()
야생 클리어까지 남은 시간을 표시하기 위해 Update 문에서 time 변수의 텍스트를 조정해줬다.
private void Update()
{
// 아직 게임 시작 안 했으면 좀 기다리기..
while (!GameManager.instance.isGameStart)
{
return;
}
time.gameObject.SetActive(true); // 게임 시작하면 활성화 켜기
if (curTime >= gamePlayTime)
{
// 플레이 시간 지나면 더이상 진행하지 않음
GameEnd();
}
if (isDead)
{
// 사망하면 더이상 진행하지 않음
return;
}
if (Input.GetMouseButtonDown(0) && jumpCount < 2)
{
// 점프 횟수 증가
jumpCount++;
// 점프 직전에 속도를 0 으로 변경
rigid.velocity = Vector3.zero;
// 위쪽으로 힘주기
rigid.AddForce(new Vector2(0, jumpPower));
// 오디오 소스 재생
PlaySound(jumpClip);
}
else if (Input.GetMouseButtonUp(0) && rigid.velocity.y > 0)
{
// 마우스 왼쪽 버튼에서 손을 떼는 순간 && 속도의 y 값이 양수(위로 상승 중)
// 현재 속도를 절반으로 변경
rigid.velocity = rigid.velocity * 0.5f;
}
// 애니메이터의 Grounded 파라미터를 isGrounded 값으로 갱신
anim.SetBool("Grounded", isGrounded);
curTime += Time.deltaTime; // 시간 더해주기
// Time Text 에 시간 반영
time.text = (int)(gamePlayTime - curTime + 1) + "s";
}
4. GameEnd()
게임이 종료(성공)될 때 브금을 멈추고 효과음을 재생시켰다.
public void GameEnd()
{
worldBGM.StopBGM(); // 스탑!
// 오디오 소스 재생
PlaySound(gameClearClip);
rigid.simulated = false;
// 여기서 게임 매니저 게임 종료(성공) 메서드 호출
GameManager.instance.WorldGameClear();
}
5. Die()
플레이어가 죽을 때 브금을 종료하고, 게임 실패 효과음을 재생시켰다. 그리고 현재 남아있던 체력 UI 를 모두 까인 모습으로 설정해줬다.
public void Die()
{
worldBGM.StopBGM(); // 스탑!
// 오디오 소스 재생
PlaySound(deathClip);
for (int i=heartIdx; i<heartUIs.Length; i++)
{
SetHeartUI(i); // 설정해용~~
}
StartCoroutine(DieCoroutine());
}
6. Attacked()
피가 깎인 걸 UI 에 반영하도록 했다. SetHeartUI 메서드를 호출했다.
private void Attacked()
{
// 오디오 소스 재생
PlaySound(attackedClip);
// 피 깎기
heart--;
SetHeartUI(heartIdx);
heartIdx++;
if (heart == 0)
{
Die(); // 죽어
return;
}
// 잠시 무적 상태로..
StartCoroutine(Invincibility());
}
7. SetHeartUI(int idx)
체력바 UI 를 설정하는 메서드이다. 체력이 까이면 호출된다.
private void SetHeartUI(int idx)
{
heartUIs[idx].GetComponent<Animator>().Play("Idle", 0, 0f); // 특정 애니메이션 초기화
heartUIs[idx].GetComponent<Animator>().enabled = false;
heartUIs[idx].transform.position = heartUIsPos[idx]; // 위치 바꿩
heartUIs[idx].GetComponent<Image>().color = new Color32(255, 255, 255, 40);
}
2.5 GameManager 스크립트
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.Rendering.Universal;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using static UnityEditor.Experimental.GraphView.Port;
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; // 싱글톤 이용하기 위함
public string curScene;
public bool isInitialized = false; // 데이터 초기화 완료 여부
private Coroutine dataSetCoroutine; // 데이터 초기화 코루틴 저장
[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; // 가구를 클릭하면 안내 판넬에 띄울 내용
public delegate void SetClayHouseLevel(int houseLevel, int clickLevel);
public SetClayHouseLevel OnSetClayHouseInfo;
// Light & UI Control
public delegate void SetLightHandler(bool flag);
public event SetLightHandler OnSetLightHandler; // 여기에 빛 관리하는 메서드 연결해놓을 것(점토의 드래그 시작되면 이 델리게이트에 연결된 메서드를 호출하도록..)
// 야생 콘텐츠 관련
[Header("World Manager")]
// 게임 시작 종료 여부
public bool isGameStart = false;
public bool isGameEnd = false;
public bool isGameClear = false;
public int worldLevel = 0; // 월드 게임 난이도(0: 쉬움, 1: 보통, 2: 어려움)
public int getClayIdx; // 얻은 점토 인덱스(이 값으로 새로운 점토를 소환할 것!) 씬 로드할 때 하면 될 듯?
public int isFirst = -1; // 잡은 점토가 처음이면 isFirst 가 해당 점토으 인덱스로.. 처음 잡힌게 아니면 걍 -1임
[Header("New Alam")]
public GameObject newAlam;
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
// 여기서 데이터 세팅하기
// DataManager 준비될 때까지 기다려!
if (dataSetCoroutine != null)
StopCoroutine(dataSetCoroutine); // 이미 시작한 코루틴 있으면 끝내고 다시 시작
dataSetCoroutine = StartCoroutine(OnSceneLoadedSetting()); // 정보 세팅
}
private void Awake()
{
// 싱글톤 이용
if (instance != null && instance != this)
{
// 만약 이미 존재하면 그냥 없애
Destroy(gameObject);
return;
}
instance = this;
DontDestroyOnLoad(gameObject); // 얘는 다른 씬으로 전환되어도 안 없앨 거임
}
private void OnEnable()
{
// 씬이 로드될 때마다 알아서 호출될 수 있도록..
SceneManager.sceneLoaded += OnSceneLoaded; // 이벤트에 메서드 연결
}
private void OnDisable()
{
// 게임 오브젝트가 비활성화 될 때 이벤트에 연결해놓은 메서드 없애기
SceneManager.sceneLoaded -= OnSceneLoaded;
}
// 씬 로드될 때 수행되어야 하는 로직 모음집..
private IEnumerator OnSceneLoadedSetting()
{
// 데이터 매니저 준비될 때까지 기다려!!!
// DataManager 초기화 완료 기다리기
while (!DataManager.instance.isInitialized)
{
yield return null;
}
Debug.Log("음 이제 DataManager 이용할 수 있어용~");
curScene = SceneManager.GetActiveScene().name; // 씬 이름 가져오기
// 씬에 따라 정보 세팅 다르게..
if (curScene == "ClayHouse")
{
// 현재 씬이 클레이 하우스인 경우에만 호출 되도록..
poolManager = GameObject.Find("PoolManager").GetComponent<PoolManager>(); // 풀매니저 찾아서 할당
gameDataUI = GameObject.Find("GameDataUIController").GetComponent<GameDataUIController>(); // 게임 데이터 UI 찾아서 할당
for (int i = 0; i < effectsPrefabs.Length; i++)
{
// 이펙트 생성해서 넣어놓기
effects[i] = Instantiate(effectsPrefabs[i], GameObject.Find(effectGameObjectNames[i]).transform);
effects[i].gameObject.SetActive(false); // 비활성화
}
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); // 게임 종료 메서드 연결
newAlam = GameObject.Find("CanvasParent").transform.Find("Canvas1").transform.Find("Left Button").transform.Find("Clay Button").transform.Find("NewAlam").gameObject;
// 메서드 연결하기
DataManager.instance.OnSave -= SetSaveData; // 중복 방지하기 위해 먼저 빼줌
DataManager.instance.OnSave += SetSaveData;
LoadDataSet(); // 데이터 반영
}
isInitialized = true; // 데이터 반영 완료
}
private void LoadDataSet()
{
if (curScene == "World") return; // 만약 현재 씬이 야생이면 그냥 빠져나가도록..
// 저장된 게임 데이터가 있는 경우 데이터 가져와서 반영
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;
SetGoldLove(); // 로드한 데이터 반영해서 데이터 UI 업데이트..
SetUpgradePanel(); // 델리게이트 호출
}
}
public void SetUpgradePanel()
{
if (curScene == "World") return; // 만약 현재 씬이 야생이면 그냥 빠져나가도록..
OnSetClayHouseInfo?.Invoke(clayHouseLevel, clayClickLevel); // UpgradePanel 클래스의 SetUpgardePanel() 메서드 호출
}
public void SetGoldLove()
{
if (curScene == "World") return; // 만약 현재 씬이 야생이면 그냥 빠져나가도록..
// 로드한 데이터에 맞게 데이터 UI 변경할 수 있도록..
// 이미 코루틴이 종료되지 않은 중에 동일한게 또 들어오면 겹쳐서 반영이 돼서 이상하게 될 수 있으므로 null 인지 판단해야함.
if (updateTextUICoroutine != null)
{
StopCoroutine(updateTextUICoroutine);
}
updateTextUICoroutine = StartCoroutine(gameDataUI.UpdateTextUI("gold", gold, gold));
updateTextUICoroutine = StartCoroutine(gameDataUI.UpdateTextUI("love", love, love));
}
// 재화 얻는 함수
public void GetGold(float capacity)
{
if (curScene == "World") return; // 만약 현재 씬이 야생이면 그냥 빠져나가도록..
// 이미 코루틴이 종료되지 않은 중에 동일한게 또 들어오면 겹쳐서 반영이 돼서 이상하게 될 수 있으므로 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)
{
if (curScene == "World") return; // 만약 현재 씬이 야생이면 그냥 빠져나가도록..
// 이미 코루틴이 종료되지 않은 중에 동일한게 또 들어오면 겹쳐서 반영이 돼서 이상하게 될 수 있으므로 null 인지 판단해야함.
if (updateTextUICoroutine != null)
{
StopCoroutine(updateTextUICoroutine);
}
updateTextUICoroutine = StartCoroutine(gameDataUI.UpdateTextUI("love", love + capacity, love));
love += capacity;
//PlayerPrefs.SetFloat("Love", love); // 데이터 저장
}
public void SetSaveData()
{
if (curScene == "World") return; // 만약 현재 씬이 야생이면 그냥 빠져나가도록..
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)
{
if (curScene == "World") return; // 만약 현재 씬이 야생이면 그냥 빠져나가도록..
// 연결된 메서드 실행시키기
OnSetInfoPanel?.Invoke(text);
}
public void SetLightAndUI(bool flag)
{
if (curScene == "World") return; // 만약 현재 씬이 야생이면 그냥 빠져나가도록..
OnSetLightHandler?.Invoke(flag); // flag 값을 전달해서 델리게이트에 연결된 메서드 호출
}
public void MoveScene()
{
// 게임 매니저와 데이터 매니저의 isInitailized 값을 다시 false 로 바꿔주기
isInitialized = false; // 다시 false 로..
DataManager.instance.isInitialized = false;
if (curScene == "ClayHouse")
{
DataManager.instance.SaveGameData(); // 씬 전환 하기 전 데이터 저장!
ResetWorldGame(); // 월드 게임 정보 초기화
curScene = "World"; // 씬 이름 바꿔주깅
// 현재가 점토 집인 경우에는 World 씬으로 이동
SceneManager.LoadScene("World");
}
else if (curScene == "World")
{
// 야생일 때는 딱히 저장할 데이터 없음
curScene = "ClayHouse";
// 현재 World 씬인 경우 점토 집 씬으로 이동
SceneManager.LoadScene("ClayHouse");
}
}
public void ResetWorldGame()
{
// 야생으로 넘어갈 때 호출되는 메서드
isGameStart = false;
isGameEnd = false;
isGameClear = false;
}
public void WorldGameClear()
{
// 야생에 성공했을 때 호출되는 메서드
isGameStart = false;
isGameEnd = true;
isGameClear = true;
}
public void WorldGameFail()
{
// 야생에 성공했을 때 호출되는 메서드
isGameStart = false;
isGameEnd = true;
isGameClear = false;
}
}
2.6 GameManager 스크립트 변경 사항 설명
1. 변수
다음과 같은 변수가 추가됐다. isFirst 는 현재 잡은 점토가 처음인지 아닌지 판단하기 위해 선언했다. 만약 처음 잡힌 점토라면 newAlam 게임 오브젝트를 활성화 하도록 했다.
public int isFirst = -1; // 잡은 점토가 처음이면 isFirst 가 해당 점토으 인덱스로.. 처음 잡힌게 아니면 걍 -1임
[Header("New Alam")]
public GameObject newAlam;
2.7 UIController 스크립트
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEditor.SearchService;
using UnityEngine;
using UnityEngine.UI;
public class UIController : MonoBehaviour
{
[Header("Heart UI")]
public GameObject[] hearts;
// 게임 시작, 종료 판넬
[Header("GamePanel")]
public GameObject gameStartPanel;
public Button gameStartButton;
public GameObject gameOverPanel;
public Button gameOverButton;
public Animator gameOverAnim;
public GameObject gameClearPanel;
public Button gameClearButton;
public Animator gameClearAnim;
public GameObject rewardClay;
public Sprite[] clayImages;
public GameObject newText; // 점토가 처음 잡힌 애면 newText 뜨도록!
private void Awake()
{
// 버튼 정보 세팅하는 메서드 호출
SetGameStartButton();
SetGameEndButton();
// 게임 시작 판넬 띄우도록
gameStartPanel.SetActive(true);
}
private IEnumerator Start()
{
// 아직 게임 시작 안 했으면 좀 기다리기..
while (!GameManager.instance.isGameStart)
{
yield return null;
}
for (int i=0; i<hearts.Length; i++)
{
// 하트 애니메이션 수행
hearts[i].GetComponent<Animator>().Play("Move", -1, i*0.2f);
}
StartCoroutine(OpenGameEndPanel()); // 게임 종료 판넬 띄우는 코루틴 시작
}
public void CloseSelf(GameObject obj)
{
// 오브젝트 비활성화
obj.SetActive(false);
}
public void GameStart()
{
// 게임 시작 신호 주기
GameManager.instance.isGameStart = true;
}
public void SetGameStartButton()
{
// 게임 시작 버튼에 연결
gameStartButton.onClick.AddListener(() => CloseSelf(gameStartPanel));
gameStartButton.onClick.AddListener(GameStart);
}
public void SetGameEndButton()
{
// 집에 돌아가야행~~
gameOverButton.onClick.AddListener(() => CloseSelf(gameOverPanel));
gameClearButton.onClick.AddListener(() => CloseSelf(gameClearPanel));
gameOverButton.onClick.AddListener(GameManager.instance.MoveScene);
gameClearButton.onClick.AddListener(GameManager.instance.MoveScene);
}
public IEnumerator OpenGameEndPanel()
{
// 게임이 종료될 때까지 기다려
while (!GameManager.instance.isGameEnd)
{
yield return null;
}
// 게임 성공 여부에 따라 판넬 띄우기
if (GameManager.instance.isGameClear)
{
gameClearPanel.SetActive(true);
// 보상 점토 이미지 설정
rewardClay.GetComponent<Image>().sprite = clayImages[GameManager.instance.getClayIdx];
rewardClay.GetComponent<Image>().SetNativeSize(); // UI 갱신
Canvas.ForceUpdateCanvases();
gameClearAnim.SetTrigger("Show"); // 애니메이션 수행
// 처음으로 잡힌 점토면 new Text
if (GameManager.instance.catchedClays[GameManager.instance.getClayIdx] == false)
{
newText.SetActive(true); // 활성화!
GameManager.instance.isFirst = GameManager.instance.getClayIdx;
}
}
else if (!GameManager.instance.isGameClear)
{
gameOverPanel.SetActive(true);
gameOverAnim.SetTrigger("Show"); // 애니메이션 수행
}
}
}
2.8 UIController 스크립트 변경 사항 설명
1. 변수
다음과 같은 변수를 추가했다.
rewardClay 는 게임 클리어 판넬에 현재 얻은 점토의 이미지를 띄우기 위해서 선언했다. clayImages 는 띄울 점토의 이미지를 가져오기 위해서 선언했다. 마지막으로 newText 는 만약 현재 잡은 점토가 처음 잡은 점토라면 new! 글자를 뜨게 하기 위해 선언했다.
public GameObject rewardClay;
public Sprite[] clayImages;
public GameObject newText; // 점토가 처음 잡힌 애면 newText 뜨도록!
2. OpenGameEndPanel()
처음으로 잡힌 점톤지 확인하고 그에 맞게 게임 클리어 판넬을 설정하는 로직을 추가했다.
public IEnumerator OpenGameEndPanel()
{
// 게임이 종료될 때까지 기다려
while (!GameManager.instance.isGameEnd)
{
yield return null;
}
// 게임 성공 여부에 따라 판넬 띄우기
if (GameManager.instance.isGameClear)
{
gameClearPanel.SetActive(true);
// 보상 점토 이미지 설정
rewardClay.GetComponent<Image>().sprite = clayImages[GameManager.instance.getClayIdx];
rewardClay.GetComponent<Image>().SetNativeSize(); // UI 갱신
Canvas.ForceUpdateCanvases();
gameClearAnim.SetTrigger("Show"); // 애니메이션 수행
// 처음으로 잡힌 점토면 new Text
if (GameManager.instance.catchedClays[GameManager.instance.getClayIdx] == false)
{
newText.SetActive(true); // 활성화!
GameManager.instance.isFirst = GameManager.instance.getClayIdx;
}
}
else if (!GameManager.instance.isGameClear)
{
gameOverPanel.SetActive(true);
gameOverAnim.SetTrigger("Show"); // 애니메이션 수행
}
}
2.9 MapSpawner 스크립트
맵 난이도에 맞게 야생 씬의 global light 색을 바꾸는 로직을 추가했다. 단순해서 딱히 설명은 길게 하지 않는다.
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.Rendering.Universal;
public class MapSpawner : MonoBehaviour
{
[Header("Map Contoller")]
public GameObject[] mapPrefabs; // 현재 맵 프리팹
public GameObject[] easyMapPrefabs;
public GameObject[] normalMapPrefabs;
public GameObject[] hardMapPrefabs;
public List<GameObject>[] pool;
public GameObject easyBackground; // 쉬움 단계 배경
public GameObject normalBackground; // 보통 단계 배경
public GameObject hardBackground; // 어려움 단계 배경
public float curTime = 0;
public float targetTime = 0.5f; // targetTime 마다 맵 생성
public int mapCount;
public int mapLevel; // 0: 쉬움, 1: 보통, 2: 어려움
private Coroutine mapCoroutine;
[Header("Light")]
public Light2D mapLight; // 맵 난이도에 따라 배경 빛 색깔 다르도록..
private void Awake()
{
mapLevel = GameManager.instance.worldLevel; // 게임 매니저의 맵 레벨로 설정해주기
Color32 col = new Color32(255, 255, 255, 255); // 디폴트값
// 맵 난이도에 맞게 mapCount 설정..
if (mapLevel == 0)
{
easyBackground.SetActive(true); // 배경 활성화
mapPrefabs = easyMapPrefabs; // 현재 팹 프리팹을 이지맵으로 설정
}
else if (mapLevel == 1)
{
normalBackground.SetActive(true); // 배경 활성화
mapPrefabs = normalMapPrefabs; // 현재 팹 프리팹을 노멀맵으로 설정
col = new Color32(150, 150, 150, 150);
}
else if (mapLevel == 2)
{
hardBackground.SetActive(true); // 배경 활성화
mapPrefabs = hardMapPrefabs; // 현재 팹 프리팹을 하드맵으로 설정
col = new Color32(120, 120, 120, 120);
}
mapLight.color = col; // 색 설정
mapCount = mapPrefabs.Length; // 크기 설정
pool = new List<GameObject>[mapCount]; // 배열 만들기
for (int i=0; i<mapCount; i++)
{
pool[i] = new List<GameObject>(); // 리스트 새로 만들기
}
}
private IEnumerator Start()
{
// 무한 루프..
// 야생 게임이 시작될 때까지 기다리기..
while (!GameManager.instance.isGameStart)
{
yield return null;
}
mapCoroutine = StartCoroutine(SpawnMap()); // 맵 생성 코루틴 시작
StartCoroutine(GameEnd()); // 게임 종료 코루틴 시작
}
private IEnumerator SpawnMap()
{
while (true)
{
int mapIdx = Random.Range(0, mapCount);
GameObject select = null;
foreach (GameObject map in pool[mapIdx])
{
// 만약 놀고 있는 맵 게임 오브젝트를 발견하면 그거 활성화
if (map.activeSelf == false)
{
select = map;
map.SetActive(true); // 맵 활성화
break;
}
}
// 발견 못 하면 새로 생성
if (select == null)
{
select = Instantiate(mapPrefabs[mapIdx], transform);
pool[mapIdx].Add(select); // 새로 생성한 게임 오브젝트를 풀에 넣기
}
yield return new WaitForSeconds(targetTime); // targetTime 만큼 기다리기
}
}
private IEnumerator GameEnd()
{
while (!GameManager.instance.isGameEnd)
yield return null;
StopCoroutine(mapCoroutine); // 게임 끝났으니까 종료!
}
}
2.10 ClayPanel 스크립트
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.UI;
public class ClayPanel : MonoBehaviour
{
[Header("UI")]
public Button pageLeftButton; // 페이지 왼쪽 버튼
public Button pageRightButton; // 페이지 오른쪽 버튼
public Button buyClayButton; // 점토 구매 버튼
public Button unlockClayButton; // 점토 해금 버튼
public Image lockedImage; // 잠금된 점토 이미지
public Image clayImage; // 점토 이미지
public Text clayName; // 점토 이름
public Text buyPrice; // 점토 가격
public Button closeButton;
public GameObject lockedPage; // 잠금 페이지
[Header("Control")]
public int pageIdx = 0; // 페이지 인덱스
public int maxIdx;
public int minIdx = 0;
[Header("Effect")]
public int effectIdx = 2;
[Header("Audio")]
private AudioSource clayPanelAudio;
public AudioClip unlockClip;
public AudioClip buyClip;
public AudioClip failClip;
public AudioClip showClip;
private void Awake()
{
clayPanelAudio = GetComponent<AudioSource>();
}
private IEnumerator Start()
{
// 게임 매니저가 준비 될 때까지 기다령~~
while (!GameManager.instance.isInitialized)
yield return null;
maxIdx = GameManager.instance.poolManager.clayPrefabs.Length;
pageLeftButton.onClick.AddListener(DownPageIdx);
pageLeftButton.onClick.AddListener(SetClayPanel);
pageRightButton.onClick.AddListener(UpPageIdx);
pageRightButton.onClick.AddListener(SetClayPanel);
// 잡아온 점토가 처음으로 잡힌 애면 별표 켜지도록..
if (GameManager.instance.isFirst != -1)
GameManager.instance.newAlam.SetActive(true);
}
private void OnEnable()
{
// 활성화 될 때 호출되는 함수
ResetClayPanel();
}
public void ResetClayPanel()
{
pageIdx = 0;
SetButtonInfo(); // 버튼 설정해주기
}
public void SetClayPanel()
{
// 새로 잡아온 점토 페이지를 확인하면 점토 구매 버튼 위 별표 비활성화
if (GameManager.instance.isFirst == pageIdx)
{
GameManager.instance.newAlam.SetActive(false);
}
Clay clay = GameManager.instance.poolManager.clayPrefabs[pageIdx].GetComponent<Clay>();
if (GameManager.instance.unLockedClays[pageIdx] == false)
{
lockedPage.SetActive(true); // 해금이 안된 경우엔 잠금 판넬 활성화..
}
else
{
lockedPage.SetActive(false); // 해금된 경우엔 잠금 판넬 비활성화..
}
// 페이지 인덱스에 맞게 설정
lockedImage.sprite = clay.clay; // 점토의 이미지 가져오기
clayImage.sprite = clay.clay;
clayName.text = clay.clayName; // 점토 이름 가져오기
buyPrice.text = clay.buyPrice + ""; // 점토 가격 가져오기
}
public void UpPageIdx()
{
if (pageIdx >= maxIdx - 1) {
GameManager.instance.StartInfoPanel("마지막 페이지입니다.");
PlaySound(failClip); // 효과음
return; // 빠져나가기..
}
pageIdx++;
SetButtonInfo();
PlaySound(showClip); // 효과음
}
public void DownPageIdx()
{
if (pageIdx <= 0) {
GameManager.instance.StartInfoPanel("처음 페이지입니다.");
PlaySound(failClip); // 효과음
return; // 빠져나가기..
}
pageIdx--;
SetButtonInfo();
PlaySound(showClip); // 효과음
}
private void SetButtonInfo()
{
ColorBlock col = unlockClayButton.colors;
// 만약 야생에서 점토를 잡아왔으면 해금하기 버튼 노란색으로 바꿔주기
if (GameManager.instance.catchedClays[pageIdx])
{
col.normalColor = new Color32(255, 220, 90, 255); // 노란색
col.highlightedColor = new Color32(255, 220, 90, 255); // 노란색
unlockClayButton.colors = col; // 색 만들어 놓은거 할당해주기
}
else
{
col.normalColor = new Color32(255, 255, 255, 255); // 흰색
col.highlightedColor = new Color32(255, 255, 255, 255); // 흰색
unlockClayButton.colors = col; // 색 만들어 놓은거 할당해주기
}
unlockClayButton.onClick.RemoveAllListeners(); // 일단 다 지웡
unlockClayButton.onClick.AddListener(() => Unlock(pageIdx)); // Unlock 메서드 추가
buyClayButton.onClick.RemoveAllListeners(); // 일단 다 지웡
buyClayButton.onClick.AddListener(() => BuyClay(pageIdx)); // BuyClay 메서드 추가
}
public void Unlock(int idx)
{
// 야생에서 아직 안 잡았으면 해금 못하도록..
if (!GameManager.instance.catchedClays[pageIdx])
{
// 안내 판넬 띄우기
GameManager.instance.StartInfoPanel("야생에서 잡아와야 해요!");
PlaySound(failClip); // 효과음
return;
}
GameManager.instance.unLockedClays[idx] = true;
PlayEffect(effectIdx); // 이펙트 소환
SetClayPanel();
PlaySound(unlockClip); // 효과음
}
public void PlayEffect(int idx)
{
if (GameManager.instance.effects[idx].gameObject.activeSelf == false)
GameManager.instance.effects[idx].gameObject.SetActive(true); // 활성화하기
GameManager.instance.effects[idx].Play();
}
public void BuyClay(int idx)
{
int price = GameManager.instance.poolManager.clayPrefabs[idx].GetComponent<Clay>().buyPrice;
// 돈이 충분하고 공간이 있으면 점토 구매
if (GameManager.instance.gold >= price && (GameManager.instance.curPossibleClayNum > GameManager.instance.poolManager.curClayNum))
{
GameManager.instance.GetGold(-price);
GameManager.instance.poolManager.GetGameObject(idx); // 동물 get!
GameManager.instance.poolManager.curClayNum++; // 점토 개수 증가
PlaySound(buyClip); // 효과음
}
else if (GameManager.instance.gold < price)
{
// 안내 판넬 띄우기
GameManager.instance.StartInfoPanel("돈이 부족해요 ㅠ_ㅠ");
PlaySound(failClip); // 효과음
return;
}
else if (GameManager.instance.curPossibleClayNum <= GameManager.instance.poolManager.curClayNum) {
// 안내 판넬 띄우기
GameManager.instance.StartInfoPanel("집이 너무 좁아요 ㅠ_ㅠ");
PlaySound(failClip); // 효과음
return;
}
}
public void ClosePanel()
{
gameObject.SetActive(false); // 활성화 끄기..
}
private void PlaySound(AudioClip clip)
{
clayPanelAudio.clip = clip;
clayPanelAudio.Play();
}
}
2.11 ClayPanel 스크립트 변경 사항 설명
1. SetClayPanel()
새로 잡아온 점토 페이지를 확인하면 점토 구매 버튼 위 별표를 비활성화 하는 로직을 추가했다.
public void SetClayPanel()
{
// 새로 잡아온 점토 페이지를 확인하면 점토 구매 버튼 위 별표 비활성화
if (GameManager.instance.isFirst == pageIdx)
{
GameManager.instance.newAlam.SetActive(false);
}
Clay clay = GameManager.instance.poolManager.clayPrefabs[pageIdx].GetComponent<Clay>();
if (GameManager.instance.unLockedClays[pageIdx] == false)
{
lockedPage.SetActive(true); // 해금이 안된 경우엔 잠금 판넬 활성화..
}
else
{
lockedPage.SetActive(false); // 해금된 경우엔 잠금 판넬 비활성화..
}
// 페이지 인덱스에 맞게 설정
lockedImage.sprite = clay.clay; // 점토의 이미지 가져오기
clayImage.sprite = clay.clay;
clayName.text = clay.clayName; // 점토 이름 가져오기
buyPrice.text = clay.buyPrice + ""; // 점토 가격 가져오기
}
3. 결과물